mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:36:23 +00:00
Compare commits
70 Commits
users/thbo
...
v2.169.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7817e1a976 | ||
|
|
d90273a068 | ||
|
|
2cdde6cb16 | ||
|
|
1f52dfa636 | ||
|
|
83b5742278 | ||
|
|
ba69b5bc93 | ||
|
|
0e8777ebda | ||
|
|
a5f06b3ec2 | ||
|
|
be325f26a6 | ||
|
|
dec260920f | ||
|
|
b0a1294ef5 | ||
|
|
3d70ef2da1 | ||
|
|
e23d68f6e2 | ||
|
|
dff1024cd3 | ||
|
|
9fc0686dc2 | ||
|
|
ab001a7004 | ||
|
|
178a618e01 | ||
|
|
dfaf6e06ee | ||
|
|
b0a71481f0 | ||
|
|
88875ca1b0 | ||
|
|
a5eb8cb5c4 | ||
|
|
41f4ca3414 | ||
|
|
aa9f5bf070 | ||
|
|
2d6042421f | ||
|
|
c8890d0f3f | ||
|
|
53fb6297cb | ||
|
|
f9b5d626c5 | ||
|
|
d34afb54b1 | ||
|
|
e291ebc58a | ||
|
|
6bec1e3bb8 | ||
|
|
0cba42590f | ||
|
|
94e7560ccd | ||
|
|
d80ab095a5 | ||
|
|
2efd6f70e2 | ||
|
|
a6f144b014 | ||
|
|
5294a3ee06 | ||
|
|
745b90a8b2 | ||
|
|
0db908da8d | ||
|
|
68de3a94be | ||
|
|
a0a590fb48 | ||
|
|
87a232c477 | ||
|
|
a3c2479a29 | ||
|
|
c45aebc9ab | ||
|
|
b676ab3d33 | ||
|
|
0a6bac355d | ||
|
|
eb78d19b17 | ||
|
|
17970ad1f9 | ||
|
|
2e0e8eb822 | ||
|
|
2a506cc556 | ||
|
|
43dd34820b | ||
|
|
746c9d9ec0 | ||
|
|
fa2ecfcc4c | ||
|
|
c59c0e2ded | ||
|
|
7a382facb3 | ||
|
|
e9ae42693f | ||
|
|
9cafe8c028 | ||
|
|
1484c3fb03 | ||
|
|
53d632706d | ||
|
|
d6179242ca | ||
|
|
0da38a6924 | ||
|
|
b19e5d7924 | ||
|
|
80ac4a8964 | ||
|
|
02639a2092 | ||
|
|
a727194742 | ||
|
|
a9c58d7398 | ||
|
|
e15414eb5e | ||
|
|
4ab1e645c3 | ||
|
|
584f6b6ca3 | ||
|
|
abc65839f3 | ||
|
|
06292aa118 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
**/bin
|
||||
**/obj
|
||||
**/libs
|
||||
**/lib
|
||||
|
||||
# editors
|
||||
**/*.xproj
|
||||
|
||||
61
docs/adrs/0263-proxy-support.md
Normal file
61
docs/adrs/0263-proxy-support.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# ADR 263: Self Hosted Runner Proxies
|
||||
|
||||
**Date**: 2019-11-13
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
- Proxy support is required for some enterprises and organizations to start using their own self hosted runners
|
||||
- While there is not a standard convention, many applications support setting proxies via the environmental variables `http_proxy`, `https_proxy`, `no_proxy`, such as curl, wget, perl, python, docker, git, R, ect
|
||||
- Some of these applications use `HTTPS_PROXY` versus `https_proxy`, but most understand or primarily support the lowercase variant
|
||||
|
||||
## Decision
|
||||
|
||||
We will update the Runner to use the conventional environment variables for proxies: `http_proxy`, `https_proxy` and `no_proxy` if they are set.
|
||||
These are described in detail below:
|
||||
- `https_proxy` a proxy URL for all https traffic. It may contain basic authentication credentials. For example:
|
||||
- http://proxy.com
|
||||
- http://127.0.0.1:8080
|
||||
- http://user:password@proxy.com
|
||||
- `http_proxy` a proxy URL for all http traffic. It may contain basic authentication credentials. For example:
|
||||
- http://proxy.com
|
||||
- http://127.0.0.1:8080
|
||||
- http://user:password@proxy.com
|
||||
- `no_proxy` a comma seperated list of hosts that should not use the proxy. An optional port may be specified
|
||||
- `google.com`
|
||||
- `yahoo.com:443`
|
||||
- `google.com,bing.com`
|
||||
|
||||
We won't use `http_proxy` for https traffic when `https_proxy` is not set, this behavior lines up with any libcurl based tools (curl, git) and wget.
|
||||
Otherwise action authors and workflow users need to adjust to differences between the runner proxy convention, and tools used by their actions and scripts.
|
||||
|
||||
Example:
|
||||
Customer set `http_proxy=http://127.0.0.1:8888` and configure the runner against `https://github.com/owner/repo`, with the `https_proxy` -> `http_proxy` fallback, the runner will connect to server without any problem. However, if user runs `git push` to `https://github.com/owner/repo`, `git` won't use the proxy since it require `https_proxy` to be set for any https traffic.
|
||||
|
||||
> `golang`, `node.js` and other dev tools from the linux community use `http_proxy` for both http and https traffic base on my research.
|
||||
|
||||
A majority of our users are using Linux where these variables are commonly required to be set by various programs. By reading these values, we simplify the process for self hosted runners to set up proxy, and expose it in a way users are already familiar with.
|
||||
|
||||
A password provided for a proxy will be masked in the logs.
|
||||
|
||||
We will support the lowercase and uppercase variants, with lowercase taking priority if both are set.
|
||||
|
||||
### No Proxy Format
|
||||
|
||||
While exact implementations are different per application on handle `no_proxy` env, most applications accept a comma separated list of hosts. Some accept wildcard characters (*). We are going to do exact case-insentive matches, and not support wildcards at this time.
|
||||
For example:
|
||||
- example.com will match example.com, foo.example.com, foo.bar.example.com
|
||||
- foo.example.com will match bar.foo.example.com and foo.example.com
|
||||
|
||||
We will not support IP addresses for `no_proxy`, only hostnames.
|
||||
|
||||
## Consequences
|
||||
|
||||
1. Enterprises and organizations needing proxy support will be able to embrace self hosted runners
|
||||
2. Users will need to set these environmental variables before configuring the runner in order to use a proxy when configuring
|
||||
3. The runner will read from the environmental variables during config and runtime and use the provided proxy if it exists
|
||||
4. Users may need to pass these environmental variables into other applications if they do not natively take these variables
|
||||
5. Action authors may need to update their workflows to react to the these environment variables
|
||||
6. We will document the way of setting environmental variables for runners using the environmental variables and how the runner uses them
|
||||
7. Like all other secrets, users will be able to relatively easily figure out proxy password if they can modify a workflow file running on a self hosted machine
|
||||
62
docs/adrs/0274-step-outcome-and-conclusion.md
Normal file
62
docs/adrs/0274-step-outcome-and-conclusion.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# ADR 0274: Step outcome and conclusion
|
||||
|
||||
**Date**: 2020-01-13
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
This ADR proposes adding `steps.<id>.outcome` and `steps.<id>.conclusion` to the steps context.
|
||||
|
||||
This allows downstream a step to run based on whether a previous step succeeded or failed.
|
||||
|
||||
Reminder, currently the steps contains `steps.<id>.outputs`.
|
||||
|
||||
## Decision
|
||||
|
||||
For steps that have completed, populate `steps.<id>.outcome` and `steps.<id>.conclusion` with one of the following values:
|
||||
|
||||
- `success`
|
||||
- `failure`
|
||||
- `cancelled`
|
||||
- `skipped`
|
||||
|
||||
When a continue-on-error step fails, the outcome will be `failure` even though the final conclusion is `success`.
|
||||
|
||||
### Example
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
|
||||
- id: experimental
|
||||
continue-on-error: true
|
||||
run: ./build.sh experimental
|
||||
|
||||
- if: ${{ steps.experimental.outcome == 'success' }}
|
||||
run: ./publish.sh experimental
|
||||
```
|
||||
|
||||
### Terminology
|
||||
|
||||
The runs API uses the term `conclusion`.
|
||||
|
||||
Therefore we use a different term `outcome` for the value prior to continue-on-error.
|
||||
|
||||
The following is a snippet from the runs API response payload:
|
||||
|
||||
```json
|
||||
"steps": [
|
||||
{
|
||||
"name": "Set up job",
|
||||
"status": "completed",
|
||||
"conclusion": "success",
|
||||
"number": 1,
|
||||
"started_at": "2020-01-09T11:06:16.000-05:00",
|
||||
"completed_at": "2020-01-09T11:06:18.000-05:00"
|
||||
},
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
- Update runner
|
||||
- Update [docs](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions#steps-context)
|
||||
263
docs/adrs/0276-problem-matchers.md
Normal file
263
docs/adrs/0276-problem-matchers.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# ADR 0276: Problem Matchers
|
||||
|
||||
**Date** 2019-06-05
|
||||
|
||||
**Status** Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Compilation failures during a CI build should surface good error messages.
|
||||
|
||||
For example, the actual compile errors from the typescript compiler should bubble as issues in the UI. And not simply "tsc exited with exit code 1".
|
||||
|
||||
VSCode has an extensible model for solving this type of problem. VSCode allows users to configure which problems matchers to use, when scanning output. For example, a user can apply the `tsc` problem matcher to receive a rich error output experience in VSCode, when compiling their typescript project.
|
||||
|
||||
The problem-matcher concept fits well with "setup" actions. For example, the `setup-nodejs` action will download node.js, add it to the PATH, and register the `tsc` problem matcher. For the duration of the job, the `tsc` problem matcher will be applied against the output.
|
||||
|
||||
## Decision
|
||||
|
||||
### Registration
|
||||
|
||||
#### Using `##` command
|
||||
|
||||
`##[add-matcher]path-to-problem-matcher-config.json`
|
||||
|
||||
Using a `##` command allows for flexibility:
|
||||
- Ad hoc scripts can register problem matchers
|
||||
- Allows problem matchers to be conditionally registered
|
||||
|
||||
Note, if a matcher with the same name is registered a second time, it will clobber the first instance.
|
||||
|
||||
#### Unregister using `##` command
|
||||
|
||||
A way out for rare cases where scoping is a problem.
|
||||
|
||||
`##[remove-matcher]owner`
|
||||
|
||||
For the this to be usable, the `owner` needs to be discoverable. Therefore, debug print the owner on registration.
|
||||
|
||||
### Single line matcher
|
||||
|
||||
Consider the output:
|
||||
|
||||
```
|
||||
[...]
|
||||
|
||||
Build FAILED.
|
||||
|
||||
"C:\temp\problemmatcher\myproject\ConsoleApp1\ConsoleApp1.sln" (default target) (1) ->
|
||||
"C:\temp\problemmatcher\myproject\ConsoleApp1\ConsoleApp1\ConsoleApp1.csproj" (default target) (2) ->
|
||||
"C:\temp\problemmatcher\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj" (default target) (3) ->
|
||||
(CoreCompile target) ->
|
||||
Class1.cs(16,24): warning CS0612: 'ClassLibrary1.Helpers.MyHelper.Name' is obsolete [C:\temp\problemmatcher\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj]
|
||||
|
||||
|
||||
"C:\temp\problemmatcher\myproject\ConsoleApp1\ConsoleApp1.sln" (default target) (1) ->
|
||||
"C:\temp\problemmatcher\myproject\ConsoleApp1\ConsoleApp1\ConsoleApp1.csproj" (default target) (2) ->
|
||||
"C:\temp\problemmatcher\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj" (default target) (3) ->
|
||||
(CoreCompile target) ->
|
||||
Helpers\MyHelper.cs(16,30): error CS1002: ; expected [C:\temp\problemmatcher\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj]
|
||||
|
||||
1 Warning(s)
|
||||
1 Error(s)
|
||||
```
|
||||
|
||||
The below match configuration uses a regular expression to discover problem lines. And the match groups are mapped into issue-properties.
|
||||
|
||||
```json
|
||||
"owner": "msbuild",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^\\s*([^:]+)\\((\\d+),(\\d+)\\): (error|warning) ([^:]+): (.*) \\[(.+)\\]$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"code": 5,
|
||||
"message": 6,
|
||||
"fromPath": 7
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The above output and match configuration produces the following matches:
|
||||
|
||||
```
|
||||
line: Class1.cs(16,24): warning CS0612: 'ClassLibrary1.Helpers.MyHelper.Name' is obsolete [C:\myrepo\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj]
|
||||
file: Class1.cs
|
||||
line: 16
|
||||
column: 24
|
||||
severity: warning
|
||||
code: CS0612
|
||||
message: 'ClassLibrary1.Helpers.MyHelper.Name' is obsolete
|
||||
fromPath: C:\myrepo\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj
|
||||
```
|
||||
|
||||
```
|
||||
line: Helpers\MyHelper.cs(16,30): error CS1002: ; expected [C:\myrepo\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj]
|
||||
file: Helpers\MyHelper.cs
|
||||
line: 16
|
||||
column: 30
|
||||
severity: error
|
||||
code: CS1002
|
||||
message: ; expected
|
||||
fromPath: C:\myrepo\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj
|
||||
```
|
||||
|
||||
Additionally the line will appear red in the web UI (prefix with `##[error]`).
|
||||
|
||||
Note, an error does not imply task failure. Exit codes communicate failure.
|
||||
|
||||
Note, strip color codes when evaluating regular expressions.
|
||||
|
||||
### Multi-line matcher
|
||||
|
||||
Consider the below output from ESLint in stylish mode. The file name is printed once, yet multiple error lines are printed.
|
||||
|
||||
```
|
||||
test.js
|
||||
1:0 error Missing "use strict" statement strict
|
||||
5:10 error 'addOne' is defined but never used no-unused-vars
|
||||
✖ 2 problems (2 errors, 0 warnings)
|
||||
```
|
||||
|
||||
The below match configuration uses multiple regular expressions, for the multiple lines.
|
||||
|
||||
And the last pattern of a multiline matcher can specify the `loop` property. This allows multiple errors to be discovered.
|
||||
|
||||
```json
|
||||
"owner": "eslint-stylish",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^([^\\s].*)$",
|
||||
"file": 1
|
||||
},
|
||||
{
|
||||
"regexp": "^\\s+(\\d+):(\\d+)\\s+(error|warning|info)\\s+(.*)\\s\\s+(.*)$",
|
||||
"line": 1,
|
||||
"column": 2,
|
||||
"severity": 3,
|
||||
"message": 4,
|
||||
"code": 5,
|
||||
"loop": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The above output and match configuration produces two matches:
|
||||
|
||||
```
|
||||
line: 1:0 error Missing "use strict" statement strict
|
||||
file: test.js
|
||||
line: 1
|
||||
column: 0
|
||||
severity: error
|
||||
message: Missing "use strict" statement
|
||||
code: strict
|
||||
```
|
||||
|
||||
```
|
||||
line: 5:10 error 'addOne' is defined but never used no-unused-vars
|
||||
file: test.js
|
||||
line: 5
|
||||
column: 10
|
||||
severity: error
|
||||
message: 'addOne' is defined but never used
|
||||
code: no-unused-vars
|
||||
```
|
||||
|
||||
Note, in the above example only the error line will appear red in the web UI. The \"file\" line will not appear red.
|
||||
|
||||
### Other details
|
||||
|
||||
#### Configuration `owner`
|
||||
|
||||
Can be used to stomp over or remove.
|
||||
|
||||
#### Rooting the file
|
||||
|
||||
The goal of the file information is to provide a hyperlink in the UI.
|
||||
|
||||
Solving this problem means:
|
||||
- Rooting the file when unrooted:
|
||||
- Use the `fromPath` if specified (assume file path)
|
||||
- Use the `github.workspace` (where the repo is cloned on disk)
|
||||
- Match against a repository to determine the relative path within the repo
|
||||
|
||||
This is a place where we diverge from VSCode. VSCode task configuration are specific to the local workspace (workspace root is known or can be specified). We're solving a more generic problem, so we need more information - specifically the `fromPath` property - in order to accurately root the path.
|
||||
|
||||
In order to avoid creating inaccurate hyperlinks on the error issues, the agent will verify the file exists and is in the main repository. Otherwise omit the file property from the error issue and debug trace what happened.
|
||||
|
||||
#### Supported severity levels
|
||||
|
||||
Ordinal ignore case:
|
||||
|
||||
- `warning`
|
||||
- `error`
|
||||
|
||||
Coalesce empty with \"error\". For any other values, omit logging an issue and debug trace what happened.
|
||||
|
||||
#### Default severity level
|
||||
|
||||
Problem matchers are unable to interpret severity strings other than `warning` and `error`. The `severity` match group expects `warning` or `error` (case insensitive).
|
||||
|
||||
However some tools indicate error/warning in different ways. For example `flake8` uses codes like `E100`, `W200`, and `F300` (error, warning, fatal, respectively).
|
||||
|
||||
Therefore, allow a property `severity`, sibling to `owner`, which identifies the default severity for the problem matcher. This allows two problem matchers are registered - one for warnings and one for errors.
|
||||
|
||||
For example, given the following `flake8` output:
|
||||
|
||||
```
|
||||
./bootcamp/settings.py:156:80: E501 line too long (94 > 79 characters)
|
||||
./bootcamp/settings.py:165:5: F403 'from local_settings import *' used; unable to detect undefined names
|
||||
```
|
||||
|
||||
Two problem matchers can be used:
|
||||
|
||||
```json
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "flake8",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+):(\\d+):(\\d+): ([EF]\\d+) (.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"code": 4,
|
||||
"message": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"owner": "flake8-warnings",
|
||||
"severity": "warning",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+):(\\d+):(\\d+): (W\\d+) (.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"code": 4,
|
||||
"message": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Mitigate regular expression denial of service (ReDos)
|
||||
|
||||
If a matcher exceeds a 1 second timeout when processing a line, retry up to two three times total.
|
||||
After three unsuccessful attempts, warn and eject the matcher. The matcher will not run again for the duration of the job.
|
||||
|
||||
### Where we diverge from VSCode
|
||||
|
||||
- We added the `fromPath` concept for rooting paths. This is done differently in VSCode, since a task is the scope (root path well known). For us, the job is the scope.
|
||||
- VSCode allows additional activation info background tasks that are always running (recompile on files changed). They allow regular expressions to define when the matcher scope begins and ends. This is an interesting concept that we could leverage to help solve our scoping problem.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Setup actions should register problem matchers
|
||||
93
docs/adrs/0277-run-action-shell-options.md
Normal file
93
docs/adrs/0277-run-action-shell-options.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# ADR 0277: Run action shell option
|
||||
|
||||
**Date** 2019-07-09
|
||||
|
||||
**Status** Accepted
|
||||
|
||||
## Context
|
||||
run-actions run scripts using a platform specific shell:
|
||||
`bash -eo pipefail` on non-windows, and `cmd.exe /c /d /s` on windows
|
||||
|
||||
The `shell` option overwrites this to allow different flags or completely different shells/interpreters
|
||||
|
||||
A small example is:
|
||||
```yml
|
||||
jobs:
|
||||
bash-job:
|
||||
actions:
|
||||
- run: echo "Hello"
|
||||
shell: bash
|
||||
python-job:
|
||||
actions:
|
||||
- run: print("Hello")
|
||||
shell: python {0}
|
||||
```
|
||||
|
||||
## Decision
|
||||
|
||||
___
|
||||
|
||||
### Shell option
|
||||
The keyword being used is `shell`
|
||||
|
||||
`shell` can be either:
|
||||
|
||||
1. Builtins / Explicitly supported keywords. It is useful to support at least `cmd`, and `powershell` on Windows. Because `cmd my_cmd_script` and `powershell my_ps1_script` are not valid the same way many Linux/cross-platform interpreters are, e.g. `bash myscript` or `python myscript`. Those tools (and potentially others) also require the correct file extension to run, or must be run in a particular way to get the exit codes consistently, so we must have first class knowledge about them. We provide default templates for these keywords as follows:
|
||||
- `cmd`: Default is: `%ComSpec% /D /E:ON /V:OFF /S /C "CALL "{0}""` where the script name is automatically appended with `.cmd` and substituted for `{0}`
|
||||
- Note this is equivalent to the default Windows behavior if no shell option is given
|
||||
- `pwsh`: Default is: `pwsh -command "& '{0}'"` where the script is automatically appended with `.ps1`
|
||||
- `powershell`: Default is: `powershell -command "& '{0}'"` where the script is automatically appended with `.ps1`
|
||||
- `bash`: Uses `bash --noprofile --norc -eo pipefail {0}`
|
||||
- The default behavior on non-Windows if no shell is given is to attempt this first
|
||||
- `sh`: Uses `sh -e {0}`
|
||||
- This is the default behavior on non-Windows if no shell is given, AND `bash` (see above) was not located on the PATH
|
||||
- `python`: `python {0}`
|
||||
- **NOTE**: The exact command ran may vary by machine. We only provide default arguments and command format for the listed shell. While the above behavior is expected on hosted machines, private runners may vary. For example, `sh` (or other commands) may actually be a link to `/bin/dash`, `/bin/bash`, or other
|
||||
|
||||
1. A template string: `command [...options] {0} [...more_options]`
|
||||
- As above, the file name of the temporary script will be templated in. This gives users more control to have options at any location relative to the script path
|
||||
- The first whitespace-delimited word of the string will be interpreted as the command
|
||||
- e.g. `python {0} arg1 arg2` or similar can be used if passing args is needed. Some shells will require other options after the filename for various reasons
|
||||
|
||||
Note that (1) simply provides defaults that are executed with the same mechanism as (2). That is:
|
||||
- A temporary script file is generated, and the path to that file is templated into the string at `{0}`
|
||||
- The first word of the formatted string is assumed to be a command, and we attempt to locate its full path
|
||||
- The fully qualified path to the command, plus the remaining arguments, is executed
|
||||
- e.g. `shell: bash` expands to `/bin/bash --noprofile --norc -eo pipefail /runner/_layout/_work/_temp/f8d4fb2b-19d9-47e6-a786-4cc538d52761.sh` on my private runner
|
||||
|
||||
At this time, **THE LIST OF WELL-KNOWN SHELL OPTIONS IS**:
|
||||
- cmd - Windows (hosted vs2017, vs2019) only
|
||||
- powershell - Windows (hosted vs2017, vs2019) only
|
||||
- sh - All hosted platforms
|
||||
- pwsh - All hosted platforms
|
||||
- bash - All hosted platforms
|
||||
- python - All hosted platforms. Can use setup-python to configure which python will be used
|
||||
___
|
||||
|
||||
### Containers
|
||||
For container jobs, `shell` should just work the same as above, transparently. We will simply `exec` the command in the job container, passing the same arguments in
|
||||
|
||||
___
|
||||
|
||||
### Exit codes / Error action preference
|
||||
|
||||
For builtin shells, we provide defaults that make the most sense for CI, running within Actions, and being executed by our runner
|
||||
|
||||
bash/sh:
|
||||
- Fail-fast behavior using `set -e o pipefail` is the default for `bash` and `shell` builtins, and by default when no option is given on non-Windows platforms
|
||||
- Users can opt out of fail-fast and take full control easily by providing a template string to the shell options, eg: `bash {0}`.
|
||||
- sh-like shells exit with the exit code of the last command executed in a script, and is our default behavior. Thus the runner reports the status of the step as fail/succeed based on this exit code
|
||||
|
||||
powershell/pwsh
|
||||
- Fail-fast behavior when possible. For `pwsh` and `powershell` builtins, we will prepend `$ErrorActionPreference = 'stop'` to script contents
|
||||
- We append `if ((Test-Path -LiteralPath variable:\LASTEXITCODE)) { exit $LASTEXITCODE }` to powershell scripts to get Action statuses to reflect the script's last exit code
|
||||
- Users can always opt out by not using the builtins, and providing a shell option like: `pwsh -File {0}`, or `powershell -Command "& '{0}'"`, depending on need
|
||||
|
||||
cmd
|
||||
- There doesnt seem to be a way to fully opt in to fail-fast behavior other than writing your script to check each error code and respond accordingly, so we cant actually provide that behavior by default, it will be completely up to the user to write this behavior into their script
|
||||
- cmd.exe will exit (return the error code to the runner) with the errorlevel of the last program it executed. This is internally consistent with the previous default behavior (sh, pwsh) and is the cmd.exe default, so we keep that behavior
|
||||
|
||||
## Consequences
|
||||
Valid `shell` options will depend on the hosted images. We will need to maintain tight image compat
|
||||
|
||||
First class support for a shell will require a major version schema change to modify. We cannot remove or modify the behavior of a well-known supported option, However, adding first class support for new shells is backwards compatible. For instance, we can add a well-known `python` option, because non-well-known options would have always needed to include `{0}`, e.g. `python {0}`
|
||||
60
docs/adrs/0278-env-context.md
Normal file
60
docs/adrs/0278-env-context.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# ADR 0278: Env Context
|
||||
|
||||
**Date**: 2019-09-30
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
User wants to reference workflow variables defined in workflow yaml file for action's input, displayName and condition.
|
||||
|
||||
## Decision
|
||||
|
||||
### Add `env` context in the runner
|
||||
|
||||
Runner will create and populate the `env` context for every job execution using following logic:
|
||||
1. On job start, create `env` context with any environment variables in the job message, these are env defined in customer's YAML file's job/workflow level `env` section.
|
||||
2. Update `env` context when customer use `::set-env::` to set env at the runner level.
|
||||
3. Update `env` context with step's `env` block before each step runs.
|
||||
|
||||
The `env` context is only available in the runner, customer can't use the `env` context in any server evaluation part, just like the `runner` context
|
||||
|
||||
Example yaml:
|
||||
```yaml
|
||||
|
||||
env:
|
||||
env1: 10
|
||||
env2: 20
|
||||
env3: 30
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
env1: 100
|
||||
env2: 200
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo ${{ env.env1 }} // 1000
|
||||
echo $env1 // 1000
|
||||
echo $env2 // 200
|
||||
echo $env3 // 30
|
||||
if: env.env2 == 200 // true
|
||||
name: ${{ env.env1 }}_${{ env.env2 }} //1000_200
|
||||
env:
|
||||
env1: 1000
|
||||
```
|
||||
|
||||
### Don't populate the `env` context with environment variables from runner machine.
|
||||
|
||||
With job container and container action, the `env` context may not have the right value customer want and will cause confusion.
|
||||
Ex:
|
||||
```yaml
|
||||
build:
|
||||
runs-on: ubuntu-latest <- $USER=runner in hosted machine
|
||||
container: ubuntu:16.04 <- $USER=root in container
|
||||
steps:
|
||||
- run: echo ${{env.USER}} <- what should customer expect this output? runner/root
|
||||
- uses: docker://ubuntu:18.04
|
||||
with:
|
||||
args: echo ${{env.USER}} <- what should customer expect this output? runner/root
|
||||
```
|
||||
71
docs/adrs/0279-hashFiles-expression-function.md
Normal file
71
docs/adrs/0279-hashFiles-expression-function.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# ADR 0279: HashFiles Expression Function
|
||||
|
||||
**Date**: 2019-09-30
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
First party action `actions/cache` needs a input which is an explicit `key` used for restoring and saving the cache. For packages caching, the most comment `key` might be the hash result of contents from all `package-lock.json` under `node_modules` folder.
|
||||
|
||||
There are serval different ways to get the hash `key` input for `actions/cache` action.
|
||||
|
||||
1. Customer calculate the `key` themselves from a different action, customer won't like this since it needs extra step for using cache feature
|
||||
```yaml
|
||||
steps:
|
||||
- run: |
|
||||
hash=some_linux_hash_method(file1, file2, file3)
|
||||
echo ::set-output name=hash::$hash
|
||||
id: createHash
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
key: ${{ steps.createHash.outputs.hash }}
|
||||
```
|
||||
|
||||
2. Make the `key` input of `actions/cache` follow certain convention to calculate hash, this limited the `key` input to a certain format customer may not want.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
key: ${{ runner.os }}|${{ github.workspace }}|**/package-lock.json
|
||||
```
|
||||
|
||||
## Decision
|
||||
|
||||
### Add hashFiles() function to expression engine for calculate files' hash
|
||||
|
||||
`hashFiles()` will only allow on runner side since it needs to read files on disk, using `hashFiles()` on any server side evaluated expression will cause runtime errors.
|
||||
|
||||
`hashFiles()` will only support hashing files under the `$GITHUB_WORKSPACE` since the expression evaluated on the runner, if customer use job container or container action, the runner won't have access to file system inside the container.
|
||||
|
||||
`hashFiles()` will only take 1 parameters:
|
||||
- `hashFiles('**/package-lock.json')` // Search files under $GITHUB_WORKSPACE and calculate a hash for them
|
||||
|
||||
**Question: Do we need to support more than one match patterns?**
|
||||
Ex: `hashFiles('**/package-lock.json', '!toolkit/core/package-lock.json', '!toolkit/io/package-lock.json')`
|
||||
Answer: Only support single match pattern for GA, we can always add later.
|
||||
|
||||
This will help customer has better experience with the `actions/cache` action's input.
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
key: ${{hashFiles('**/package-lock.json')}}-${{github.ref}}-${{runner.os}}
|
||||
```
|
||||
|
||||
For search pattern, we will use basic globbing (`*` `?` and `[]`) and globstar (`**`).
|
||||
|
||||
Additional pattern details:
|
||||
- Root relative paths with `github.workspace` (the main repo)
|
||||
- Make `*` match files that start with `.`
|
||||
- Case insensitive on Windows
|
||||
- Accept `\` or `/` path separators on Windows
|
||||
|
||||
Hashing logic:
|
||||
1. Get all files under `$GITHUB_WORKSPACE`.
|
||||
2. Use search pattern filter all files to get files that matches the search pattern. (search pattern only apply to file path not folder path)
|
||||
3. Sort all matched files by full file path in alphabet order.
|
||||
4. Use SHA256 algorithm to hash each matched file and store hash result.
|
||||
5. Use SHA256 to hash all stored files' hash results to get the final 64 chars hash result.
|
||||
|
||||
**Question: Should we include the folder structure info into the hash?**
|
||||
Answer: No
|
||||
30
docs/adrs/0280-command-input-echoing.md
Normal file
30
docs/adrs/0280-command-input-echoing.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# ADR 0280: Echoing of Command Input
|
||||
|
||||
**Date**: 2019-11-04
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Command echoing as a default behavior tends to clutter the user logs, so we want to swap to a system where users have to opt in to see this information.
|
||||
|
||||
Command outputs will still be echoed in the case there are any errors processing such commands. This is so the end user can have more context on why the command failed and help with troubleshooting.
|
||||
|
||||
Echo output in the user logs can be explicitly controlled by the new commands `::echo::on` and `::echo::off`. By default, echoing is enabled if `ACTIONS_STEP_DEBUG` secret is enabled, otherwise echoing is disabled.
|
||||
|
||||
## Decision
|
||||
- The only commands that currently echo output are
|
||||
- `remove-matcher`
|
||||
- `add-matcher`
|
||||
- `add-path`
|
||||
- These will no longer echo the command, if processed successfully
|
||||
- All commands echo the input when any of these conditions is fulfilled:
|
||||
1. When such commands fail with an error
|
||||
2. When `::echo::on` is set
|
||||
3. When the `ACTIONS_STEP_DEBUG` is set, and echoing hasn't been explicitly disabled with `::echo::off`
|
||||
- There are a few commands that won't be echoed, even when echo is enabled. These are (as of 2019/11/04):
|
||||
- `add-mask`
|
||||
- `debug`
|
||||
- `warning`
|
||||
- `error`
|
||||
- The three commands above will not echo, either because echoing the command would leak secrets (e.g. `add-mask`), or it would not add any additional troubleshooting information to the logs (e.g. `debug`). It's expected that future commands would follow these "echo-suppressing" guidelines as well. Echo-suppressed commands are still free to output other information to the logs, as deemed fit.
|
||||
48
docs/adrs/0297-base64-masking-trailing-characters.md
Normal file
48
docs/adrs/0297-base64-masking-trailing-characters.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# ADR 0297: Base64 Masking Trailing Characters
|
||||
|
||||
**Date** 2020-01-21
|
||||
|
||||
**Status** Proposed
|
||||
|
||||
## Context
|
||||
|
||||
The Runner registers a number of Value Encoders, which mask various encodings of a provided secret. Currently, we register a 3 base64 Encoders:
|
||||
- The base64 encoded secret
|
||||
- The secret with the first character removed then base64 encoded
|
||||
- The secret with the first two characters removed then base64 encoded
|
||||
|
||||
This gives us good coverage across the board for secrets and secrets with a prefix (i.e. `base64($user:$pass)`).
|
||||
|
||||
However, we don't have great coverage for cases where the secret has a string appended to it before it is base64 encoded (i.e.: `base64($pass\n))`).
|
||||
|
||||
Most notably we've seen this as a result of user error where a user accidentially appends a newline or space character before encoding their secret in base64.
|
||||
|
||||
## Decision
|
||||
|
||||
### Trim end characters
|
||||
|
||||
We are going to modify all existing base64 encoders to trim information before registering as a secret.
|
||||
We will trim:
|
||||
- `=` from the end of all base64 strings. This is a padding character that contains no information.
|
||||
- Based on the number of `=`'s at the end of a base64 string, a malicious user could predict the length of the original secret modulo 3.
|
||||
- If a user saw `***==`, they would know the secret could be 1,4,7,10... characters.
|
||||
- If a string contains `=` we will also trim the last non-padding character from the base64 secret.
|
||||
- This character can change if a string is appended to the secret before the encoding.
|
||||
|
||||
|
||||
### Register a fourth encoder
|
||||
|
||||
We will also add back in the original base64 encoded secret encoder for four total encoders:
|
||||
- The base64 encoded secret
|
||||
- The base64 encoded secret trimmed
|
||||
- The secret with the first character removed then base64 encoded and trimmed
|
||||
- The secret with the first two characters removed then base64 encoded and trimmed
|
||||
|
||||
This allows us to fully cover the most common scenario where a user base64 encodes their secret and expects the entire thing to be masked.
|
||||
This will result in us only revealing length or bit information when a prefix or suffix is added to a secret before encoding.
|
||||
|
||||
## Consequences
|
||||
|
||||
- In the case where a secret has a prefix or suffix added before base64 encoding, we may now reveal up to 20 bits of information and the length of the original string modulo 3, rather then the original 16 bits and no length information
|
||||
- Secrets with a suffix appended before encoding will now be masked across the board. Previously it was only masked if it was a multiple of 3 characters
|
||||
- Performance will suffer in a neglible way
|
||||
35
docs/adrs/0354-runner-machine-info.md
Normal file
35
docs/adrs/0354-runner-machine-info.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# ADR 354: Expose runner machine info
|
||||
|
||||
**Date**: 2020-03-02
|
||||
|
||||
**Status**: Pending
|
||||
|
||||
## Context
|
||||
|
||||
- Provide a mechanism in the runner to include extra information in `Set up job` step's log.
|
||||
Ex: Include OS/Software info from Hosted image.
|
||||
|
||||
## Decision
|
||||
|
||||
The runner will look for a file `.setup_info` under the runner's root directory, The file can be a JSON with a simple schema.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"group": "OS Detail",
|
||||
"detail": "........"
|
||||
},
|
||||
{
|
||||
"group": "Software Detail",
|
||||
"detail": "........"
|
||||
}
|
||||
]
|
||||
```
|
||||
The runner will use `##[group]` and `##[endgroup]` to fold all detail info into an expandable group.
|
||||
|
||||
Both [virtual-environments](https://github.com/actions/virtual-environments) and self-hosted runners can use this mechanism to add extra logging info to the `Set up job` step's log.
|
||||
|
||||
## Consequences
|
||||
|
||||
1. Change the runner to best effort read/parse `.extra_setup_info` file under runner root directory.
|
||||
2. [virtual-environments](https://github.com/actions/virtual-environments) generate the file during image generation.
|
||||
3. Change MMS provisioner to properly copy the file to runner root directory at runtime.
|
||||
75
docs/adrs/0361-wrapper-action.md
Normal file
75
docs/adrs/0361-wrapper-action.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# ADR 361: Wrapper Action
|
||||
|
||||
**Date**: 2020-03-06
|
||||
|
||||
**Status**: Pending
|
||||
|
||||
## Context
|
||||
|
||||
In addition to action's regular execution, action author may wants their action has a chance to participate in:
|
||||
- Job initialize
|
||||
My Action will collect machine resource usage (CPU/RAM/Disk) during a workflow job execution, we need to start perf recorder at the begin of the job.
|
||||
- Job cleanup
|
||||
My Action will dirty local workspace or machine environment during execution, we need to cleanup these changes at the end of the job.
|
||||
Ex: `actions/checkout@v2` will write `github.token` into local `.git/config` during execution, it has post job cleanup defined to undo the changes.
|
||||
|
||||
## Decision
|
||||
|
||||
### Add `pre` and `post` execution to action
|
||||
|
||||
Node Action Example:
|
||||
|
||||
```yaml
|
||||
name: 'My action with pre'
|
||||
description: 'My action with pre'
|
||||
runs:
|
||||
using: 'node12'
|
||||
pre: 'setup.js'
|
||||
pre-if: 'success()' // Optional
|
||||
main: 'index.js'
|
||||
post: 'cleanup.js'
|
||||
post-if: 'success()' // Optional
|
||||
```
|
||||
|
||||
Container Action Example:
|
||||
|
||||
```yaml
|
||||
name: 'My action with pre'
|
||||
description: 'My action with pre'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'mycontainer:latest'
|
||||
pre-entrypoint: 'setup.sh'
|
||||
pre-if: 'success()' // Optional
|
||||
entrypoint: 'entrypoint.sh'
|
||||
post-entrypoint: 'cleanup.sh'
|
||||
post-if: 'success()' // Optional
|
||||
```
|
||||
|
||||
Both `pre` and `post` will has default `pre-if/post-if` sets to `always()`.
|
||||
Setting `pre` to `always()` will make sure no matter what condition evaluate result the `main` gets at runtime, the `pre` has always run already.
|
||||
`pre` executes in order of how the steps are defined.
|
||||
`pre` will always be added to job steps list during job setup.
|
||||
> Action referenced from local repository (`./my-action`) won't get `pre` setup correctly since the repository haven't checkout during job initialize.
|
||||
> We can't use GitHub api to download the repository since there is a about 3 mins delay between `git push` and the new commit available to download using GitHub api.
|
||||
|
||||
`post` will be pushed into a `poststeps` stack lazily when the action's `pre` or `main` execution passed `if` condition check and about to run, you can't have an action that only contains a `post`, we will pop and run each `post` after all `pre` and `main` finished.
|
||||
> Currently `post` works for both repository action (`org/repo@v1`) and local action (`./my-action`)
|
||||
|
||||
Valid action:
|
||||
- only has `main`
|
||||
- has `pre` and `main`
|
||||
- has `main` and `post`
|
||||
- has `pre`, `main` and `post`
|
||||
|
||||
Invalid action:
|
||||
- only has `pre`
|
||||
- only has `post`
|
||||
- has `pre` and `post`
|
||||
|
||||
Potential downside of introducing `pre`:
|
||||
|
||||
- Extra magic wrt step order. Users should control the step order. Especially when we introduce templates.
|
||||
- Eliminates the possibility to lazily download the action tarball, since `pre` always run by default, we have to download the tarball to check whether action defined a `pre`
|
||||
- `pre` doesn't work with local action, we suggested customer use local action for testing their action changes, ex CI for their action, to avoid delay between `git push` and GitHub repo tarball download api.
|
||||
- Condition on the `pre` can't be controlled using dynamic step outputs. `pre` executes too early.
|
||||
56
docs/adrs/0397-runner-registration-labels.md
Normal file
56
docs/adrs/0397-runner-registration-labels.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# ADR 0397: Support adding custom labels during runner config
|
||||
**Date**: 2020-03-30
|
||||
|
||||
**Status**: Approved
|
||||
|
||||
## Context
|
||||
|
||||
Since configuring self-hosted runners is commonly automated via scripts, the labels need to be able to be created during configuration. The runner currently registers the built-in labels (os, arch) during registration but does not accept labels via command line args to extend the set registered.
|
||||
|
||||
See Issue: https://github.com/actions/runner/issues/262
|
||||
|
||||
This is another version of [ADR275](https://github.com/actions/runner/pull/275)
|
||||
|
||||
## Decision
|
||||
|
||||
This ADR proposes that we add a `--labels` option to `config`, which could be used to add custom additional labels to the configured runner.
|
||||
|
||||
For example, to add a single extra label the operator could run:
|
||||
```bash
|
||||
./config.sh --labels mylabel
|
||||
```
|
||||
> Note: the current runner command line parsing and envvar override algorithm only supports a single argument (key).
|
||||
|
||||
This would add the label `mylabel` to the runner, and enable users to select the runner in their workflow using this label:
|
||||
```yaml
|
||||
runs-on: [self-hosted, mylabel]
|
||||
```
|
||||
|
||||
To add multiple labels the operator could run:
|
||||
```bash
|
||||
./config.sh --labels mylabel,anotherlabel
|
||||
```
|
||||
> Note: the current runner command line parsing and envvar override algorithm only supports a single argument (key).
|
||||
|
||||
This would add the label `mylabel` and `anotherlabel` to the runner, and enable users to select the runner in their workflow using this label:
|
||||
```yaml
|
||||
runs-on: [self-hosted, mylabel, anotherlabel]
|
||||
```
|
||||
|
||||
It would not be possible to remove labels from an existing runner using `config.sh`, instead labels would have to be removed using the GitHub UI.
|
||||
|
||||
The labels argument will split on commas, trim and discard empty strings. That effectively means don't use commans in unattended config label names. Alternatively we could choose to escape commans but it's a nice to have.
|
||||
|
||||
## Replace
|
||||
|
||||
If an existing runner exists and the option to replace is chosen (interactively of via unattend as in this scenario), then the labels will be replaced / overwritten (not merged).
|
||||
|
||||
## Overriding built-in labels
|
||||
|
||||
Note that it is possible to register "built-in" hosted labels like `ubuntu-latest` and is not considered an error. This is an effective way for the org / runner admin to dictate by policy through registration that this set of runners will be used without having to edit all the workflow files now and in the future.
|
||||
|
||||
We will also not make other restrictions such as limiting explicitly adding os / arch labels and validating. We will assume that explicit labels were added for a reason and not restricting offers the most flexibility and future proofing / compat.
|
||||
|
||||
## Consequences
|
||||
|
||||
The ability to add custom labels to a self-hosted runner would enable most scenarios where job runner selection based on runner capabilities or characteristics are required.
|
||||
@@ -44,7 +44,7 @@ Sample developer flow:
|
||||
```bash
|
||||
git clone https://github.com/actions/runner
|
||||
cd ./src
|
||||
./dev.(sh/cmd) layout # the runner that build from source is in {root}/_layout
|
||||
./dev.(sh/cmd) layout # the runner that built from source is in {root}/_layout
|
||||
<make code changes>
|
||||
./dev.(sh/cmd) build # {root}/_layout will get updated
|
||||
./dev.(sh/cmd) test # run all unit tests before git commit/push
|
||||
|
||||
61
docs/design/auth.md
Normal file
61
docs/design/auth.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Runner Authentication and Authorization
|
||||
|
||||
## Goals
|
||||
- Support runner installs in untrusted domains.
|
||||
- The account that configures or runs the runner process is not relevant for accessing GitHub resources.
|
||||
- Accessing GitHub resources is done with a per-job token which expires when job completes.
|
||||
- The token is granted to trusted parts of the system including the runner, actions and script steps specified by the workflow author as trusted.
|
||||
- All OAuth tokens that come from the Token Service that the runner uses to access Actions Service resources are the same. It's just the scope and expiration of the token that may vary.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuring a self-hosted runner is [covered here in the documentation](https://help.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners).
|
||||
|
||||
Configuration is done with the user being authenticated via a time-limited, GitHub runner registration token.
|
||||
|
||||
*Your credentials are never used for registering the runner with the service.*
|
||||
|
||||

|
||||
|
||||
During configuration, an RSA public/private key pair is created, the private key is stored in file on disk. On Windows, the content is protected with DPAPI (machine level encrypted - runner only valid on that machine) and on Linux/OSX with `chmod` permissions.
|
||||
|
||||
Using your credentials, the runner is registered with the service by sending the public key to the service which adds that runner to the pool and stores the public key, the Token Service will generate a `clientId` associated with the public key.
|
||||
|
||||
## Start and Listen
|
||||
|
||||
After configuring the runner, the runner can be started interactively (`./run.cmd` or `./run.sh`) or as a service.
|
||||
|
||||

|
||||
|
||||
On start, the runner listener process loads the RSA private key (on Windows decrypting with machine key DPAPI), and asks the Token Service for an OAuth token which is signed with the RSA private key.
|
||||
The server then responds with an OAuth token that grants permission to access the message queue (HTTP long poll), allowing the runner to acquire the messages it will eventually run.
|
||||
|
||||
## Run a workflow
|
||||
|
||||
When a workflow is run, its labels are evaluated, it is matched to a runner and a message is placed in a queue of messages for that runner.
|
||||
The runner then starts listening for jobs via the message queue HTTP long poll.
|
||||
The message is encrypted with the runner's public key, stored during runner configuration.
|
||||
|
||||

|
||||
|
||||
A workflow is queued as a result of a triggered [event](https://help.github.com/en/actions/reference/events-that-trigger-workflows). Workflows can be scheduled to [run at specific UTC times](https://help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule) using POSIX `cron` syntax.
|
||||
An [OAuth token](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) is generated, granting limited access to the host in Actions Service associated with the github.com repository/organization.
|
||||
The lifetime of the OAuth token is the lifetime of the run or at most the [job timeout (default: 6 hours)](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes), plus 10 additional minutes.
|
||||
|
||||
## Accessing GitHub resources
|
||||
|
||||
The job message sent to the runner contains the OAuth token to talk back to the Actions Service.
|
||||
The runner listener parent process will spawn a runner worker process for that job and send it the job message over IPC.
|
||||
The token is never persisted.
|
||||
|
||||
Each action is run as a unique subprocess.
|
||||
The encrypted access token will be provided as an environment variable in each action subprocess.
|
||||
The token is registered with the runner as a secret and scrubbed from the logs as they are written.
|
||||
|
||||
Authentication in a workflow run to github.com can be accomplished by using the [`GITHUB_TOKEN`](https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#about-the-github_token-secret)) secret. This token expires after 60 minutes. Please note that this token is different from the OAuth token that the runner uses to talk to the Actions Service.
|
||||
|
||||
## Hosted runner authentication
|
||||
|
||||
Hosted runner authentication differs from self-hosted authentication in that runners do not undergo a registration process, but instead, the hosted runners get the OAuth token directly by reading the `.credentials` file. The scope of this particular token is limited for a given workflow job execution, and the token is revoked as soon as the job is finished.
|
||||
|
||||

|
||||
BIN
docs/res/hosted-config-start.png
Normal file
BIN
docs/res/hosted-config-start.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
52
docs/res/runner-auth-diags.txt
Normal file
52
docs/res/runner-auth-diags.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
# Markup used to generate the runner auth diagrams: https://websequencediagrams.com
|
||||
|
||||
title Runner Configuration (self-hosted only)
|
||||
|
||||
note left of Runner: GitHub repo URL as input
|
||||
Runner->github.com: Retrieve Actions Service access using runner registration token
|
||||
github.com->Runner: Access token for Actions Service
|
||||
note left of Runner: Generate RSA key pair
|
||||
note left of Runner: Store encrypted RSA private key on disk
|
||||
Runner->Actions Service: Register runner using Actions Service access token
|
||||
note right of Runner: Runner name, RSA public key sent
|
||||
note right of Actions Service: Public key stored
|
||||
Actions Service->Token Service: Register runner as an app along with the RSA public key
|
||||
note right of Token Service: Public key stored
|
||||
Token Service->Actions Service: Client Id for the runner application
|
||||
Actions Service->Runner: Client Id and Token Endpoint URL
|
||||
note left of Runner: Store runner configuration info into .runner file
|
||||
note left of Runner: Store Token registration info into .credentials file
|
||||
|
||||
title Runner Start and Running (self-hosted only)
|
||||
|
||||
Runner.Listener->Runner.Listener: Start
|
||||
note left of Runner.Listener: Load config info from .runner
|
||||
note left of Runner.Listener: Load token registration from .credentials
|
||||
Runner.Listener->Token Service: Exchange OAuth token (happens every 50 mins)
|
||||
note right of Runner.Listener: Construct JWT token, use Client Id signed by RSA private key
|
||||
note left of Actions Service: Find corresponding RSA public key, use Client Id\nVerify JWT token's signature
|
||||
Token Service->Runner.Listener: OAuth token with limited permission and valid for 50 mins
|
||||
Runner.Listener->Actions Service: Connect to Actions Service with OAuth token
|
||||
Actions Service->Runner.Listener: Workflow job
|
||||
|
||||
title Running workflow
|
||||
|
||||
Runner.Listener->Service (Message Queue): Get message
|
||||
note right of Runner.Listener: Authenticate with exchanged OAuth token
|
||||
Event->Actions Service: Queue workflow
|
||||
Actions Service->Actions Service: Generate OAuth token per job
|
||||
Actions Service->Actions Service: Build job message with the OAuth token
|
||||
Actions Service->Actions Service: Encrypt job message with the target runner's public key
|
||||
Actions Service->Service (Message Queue): Send encrypted job message to runner
|
||||
Service (Message Queue)->Runner.Listener: Send job
|
||||
note right of Runner.Listener: Decrypt message with runner's private key
|
||||
Runner.Listener->Runner.Worker: Create worker process per job and run the job
|
||||
|
||||
title Runner Configuration, Start and Running (hosted only)
|
||||
|
||||
Machine Management Service->Runner.Listener: Construct .runner configuration file, store token in .credentials
|
||||
Runner.Listener->Runner.Listener: Start
|
||||
note left of Runner.Listener: Load config info from .runner
|
||||
note left of Runner.Listener: Load OAuth token from .credentials
|
||||
Runner.Listener->Actions Service: Connect to Actions Service with OAuth token in .credentials
|
||||
Actions Service->Runner.Listener: Workflow job
|
||||
BIN
docs/res/self-hosted-config.png
Normal file
BIN
docs/res/self-hosted-config.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/res/self-hosted-start.png
Normal file
BIN
docs/res/self-hosted-start.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/res/workflow-run.png
Normal file
BIN
docs/res/workflow-run.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -1,17 +1,14 @@
|
||||
## Features
|
||||
- Remove runner flow: Change from PAT to "deletion token" in prompt (#225)
|
||||
- Expose github.run_id and github.run_number to action runtime env. (#224)
|
||||
|
||||
- Runner support for GHES Alpha (#381 #386 #390 #393 $401)
|
||||
- Allow secrets context in Container.env (#388)
|
||||
## Bugs
|
||||
- Clean up error messages for container scenarios (#221)
|
||||
- Pick shell from prependpath (#231)
|
||||
|
||||
- Raise warning when volume mount root. (#413)
|
||||
- Fix typo (#394)
|
||||
## Misc
|
||||
- Runner code cleanup (#218 #227, #228, #229, #230)
|
||||
- Consume dotnet core 3.1 in runner. (#213)
|
||||
- N/A
|
||||
|
||||
## Windows x64
|
||||
We recommend configuring the runner under "<DRIVE>:\actions-runner". This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows
|
||||
We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows
|
||||
```
|
||||
// Create a folder under the drive root
|
||||
mkdir \actions-runner ; cd \actions-runner
|
||||
@@ -19,7 +16,7 @@ mkdir \actions-runner ; cd \actions-runner
|
||||
Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-win-x64-<RUNNER_VERSION>.zip -OutFile actions-runner-win-x64-<RUNNER_VERSION>.zip
|
||||
// Extract the installer
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory("$HOME\Downloads\actions-runner-win-x64-<RUNNER_VERSION>.zip", "$PWD")
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD\actions-runner-win-x64-<RUNNER_VERSION>.zip", "$PWD")
|
||||
```
|
||||
|
||||
## OSX
|
||||
@@ -28,7 +25,7 @@ Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
||||
// Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
curl -O https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
tar xzf ./actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
@@ -39,7 +36,7 @@ tar xzf ./actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
curl -O https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
tar xzf ./actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
@@ -50,7 +47,7 @@ tar xzf ./actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
curl -O https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
tar xzf ./actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
@@ -61,7 +58,7 @@ tar xzf ./actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
// Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
curl -O https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
tar xzf ./actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
|
||||
11
src/Misc/dotnet-install.sh
vendored
11
src/Misc/dotnet-install.sh
vendored
@@ -728,11 +728,12 @@ downloadcurl() {
|
||||
# Append feed_credential as late as possible before calling curl to avoid logging feed_credential
|
||||
remote_path="${remote_path}${feed_credential}"
|
||||
|
||||
local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs "
|
||||
local failed=false
|
||||
if [ -z "$out_path" ]; then
|
||||
curl --retry 10 -sSL -f --create-dirs "$remote_path" || failed=true
|
||||
curl $curl_options "$remote_path" || failed=true
|
||||
else
|
||||
curl --retry 10 -sSL -f --create-dirs -o "$out_path" "$remote_path" || failed=true
|
||||
curl $curl_options -o "$out_path" "$remote_path" || failed=true
|
||||
fi
|
||||
if [ "$failed" = true ]; then
|
||||
say_verbose "Curl download failed"
|
||||
@@ -748,12 +749,12 @@ downloadwget() {
|
||||
|
||||
# Append feed_credential as late as possible before calling wget to avoid logging feed_credential
|
||||
remote_path="${remote_path}${feed_credential}"
|
||||
|
||||
local wget_options="--tries 20 --waitretry 2 --connect-timeout 15 "
|
||||
local failed=false
|
||||
if [ -z "$out_path" ]; then
|
||||
wget -q --tries 10 -O - "$remote_path" || failed=true
|
||||
wget -q $wget_options -O - "$remote_path" || failed=true
|
||||
else
|
||||
wget --tries 10 -O "$out_path" "$remote_path" || failed=true
|
||||
wget $wget_options -O "$out_path" "$remote_path" || failed=true
|
||||
fi
|
||||
if [ "$failed" = true ]; then
|
||||
say_verbose "Wget download failed"
|
||||
|
||||
3
src/Misc/expressionFunc/hashFiles/.eslintignore
Normal file
3
src/Misc/expressionFunc/hashFiles/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
59
src/Misc/expressionFunc/hashFiles/.eslintrc.json
Normal file
59
src/Misc/expressionFunc/hashFiles/.eslintrc.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"plugins": ["jest", "@typescript-eslint"],
|
||||
"extends": ["plugin:github/es6"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"eslint-comments/no-use": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-console": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-ignore": "error",
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/camelcase": "error",
|
||||
"@typescript-eslint/class-name-casing": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||
"@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"@typescript-eslint/no-for-in-array": "error",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-object-literal-type-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-for-of": "warn",
|
||||
"@typescript-eslint/prefer-function-type": "warn",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-interface": "error",
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
"@typescript-eslint/restrict-plus-operands": "error",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": ["error", "never"],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unbound-method": "error"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest/globals": true
|
||||
}
|
||||
}
|
||||
3
src/Misc/expressionFunc/hashFiles/.prettierignore
Normal file
3
src/Misc/expressionFunc/hashFiles/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
11
src/Misc/expressionFunc/hashFiles/.prettierrc.json
Normal file
11
src/Misc/expressionFunc/hashFiles/.prettierrc.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid",
|
||||
"parser": "typescript"
|
||||
}
|
||||
1
src/Misc/expressionFunc/hashFiles/README.md
Normal file
1
src/Misc/expressionFunc/hashFiles/README.md
Normal file
@@ -0,0 +1 @@
|
||||
To update hashFiles under `Misc/layoutbin` run `npm install && npm run all`
|
||||
2616
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
Normal file
2616
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
src/Misc/expressionFunc/hashFiles/package.json
Normal file
35
src/Misc/expressionFunc/hashFiles/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "hashFiles",
|
||||
"version": "1.0.0",
|
||||
"description": "GitHub Actions HashFiles() expression function",
|
||||
"main": "lib/hashFiles.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"format-check": "prettier --check **/*.ts",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"pack": "ncc build -o ../../layoutbin/hashFiles",
|
||||
"all": "npm run build && npm run format && npm run lint && npm run pack"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/actions/runner.git"
|
||||
},
|
||||
"keywords": [
|
||||
"actions"
|
||||
],
|
||||
"author": "GitHub Actions",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/glob": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.7.12",
|
||||
"@typescript-eslint/parser": "^2.8.0",
|
||||
"@zeit/ncc": "^0.20.5",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-github": "^2.0.0",
|
||||
"prettier": "^1.19.1",
|
||||
"typescript": "^3.6.4"
|
||||
}
|
||||
}
|
||||
55
src/Misc/expressionFunc/hashFiles/src/hashFiles.ts
Normal file
55
src/Misc/expressionFunc/hashFiles/src/hashFiles.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as glob from '@actions/glob'
|
||||
import * as crypto from 'crypto'
|
||||
import * as fs from 'fs'
|
||||
import * as stream from 'stream'
|
||||
import * as util from 'util'
|
||||
import * as path from 'path'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
// arg0 -> node
|
||||
// arg1 -> hashFiles.js
|
||||
// env[followSymbolicLinks] = true/null
|
||||
// env[patterns] -> glob patterns
|
||||
let followSymbolicLinks = false
|
||||
const matchPatterns = process.env.patterns || ''
|
||||
if (process.env.followSymbolicLinks === 'true') {
|
||||
console.log('Follow symbolic links')
|
||||
followSymbolicLinks = true
|
||||
}
|
||||
|
||||
console.log(`Match Pattern: ${matchPatterns}`)
|
||||
let hasMatch = false
|
||||
const githubWorkspace = process.cwd()
|
||||
const result = crypto.createHash('sha256')
|
||||
let count = 0
|
||||
const globber = await glob.create(matchPatterns, {followSymbolicLinks})
|
||||
for await (const file of globber.globGenerator()) {
|
||||
console.log(file)
|
||||
if (!file.startsWith(`${githubWorkspace}${path.sep}`)) {
|
||||
console.log(`Ignore '${file}' since it is not under GITHUB_WORKSPACE.`)
|
||||
continue
|
||||
}
|
||||
if (fs.statSync(file).isDirectory()) {
|
||||
console.log(`Skip directory '${file}'.`)
|
||||
continue
|
||||
}
|
||||
const hash = crypto.createHash('sha256')
|
||||
const pipeline = util.promisify(stream.pipeline)
|
||||
await pipeline(fs.createReadStream(file), hash)
|
||||
result.write(hash.digest())
|
||||
count++
|
||||
if (!hasMatch) {
|
||||
hasMatch = true
|
||||
}
|
||||
}
|
||||
result.end()
|
||||
|
||||
if (hasMatch) {
|
||||
console.log(`Find ${count} files to hash.`)
|
||||
console.error(`__OUTPUT__${result.digest('hex')}__OUTPUT__`)
|
||||
} else {
|
||||
console.error(`__OUTPUT____OUTPUT__`)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
12
src/Misc/expressionFunc/hashFiles/tsconfig.json
Normal file
12
src/Misc/expressionFunc/hashFiles/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"outDir": "./lib", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
2623
src/Misc/layoutbin/hashFiles/index.js
Normal file
2623
src/Misc/layoutbin/hashFiles/index.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,6 @@
|
||||
<packageSources>
|
||||
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
|
||||
<clear />
|
||||
<add key="dotnet-core" value="https://www.myget.org/F/dotnet-core/api/v3/index.json" />
|
||||
<add key="dotnet-buildtools" value="https://www.myget.org/F/dotnet-buildtools/api/v3/index.json" />
|
||||
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
||||
@@ -9,26 +9,27 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
private static readonly EscapeMapping[] _escapeMappings = new[]
|
||||
{
|
||||
new EscapeMapping(token: "%", replacement: "%25"),
|
||||
new EscapeMapping(token: ";", replacement: "%3B"),
|
||||
new EscapeMapping(token: "\r", replacement: "%0D"),
|
||||
new EscapeMapping(token: "\n", replacement: "%0A"),
|
||||
new EscapeMapping(token: "]", replacement: "%5D"),
|
||||
new EscapeMapping(token: "%", replacement: "%25"),
|
||||
};
|
||||
|
||||
private static readonly EscapeMapping[] _escapeDataMappings = new[]
|
||||
{
|
||||
new EscapeMapping(token: "\r", replacement: "%0D"),
|
||||
new EscapeMapping(token: "\n", replacement: "%0A"),
|
||||
new EscapeMapping(token: "%", replacement: "%25"),
|
||||
};
|
||||
|
||||
private static readonly EscapeMapping[] _escapePropertyMappings = new[]
|
||||
{
|
||||
new EscapeMapping(token: "%", replacement: "%25"),
|
||||
new EscapeMapping(token: "\r", replacement: "%0D"),
|
||||
new EscapeMapping(token: "\n", replacement: "%0A"),
|
||||
new EscapeMapping(token: ":", replacement: "%3A"),
|
||||
new EscapeMapping(token: ",", replacement: "%2C"),
|
||||
new EscapeMapping(token: "%", replacement: "%25"),
|
||||
};
|
||||
|
||||
private readonly Dictionary<string, string> _properties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace GitHub.Runner.Common
|
||||
[DataContract]
|
||||
public sealed class RunnerSettings
|
||||
{
|
||||
[DataMember(Name = "IsHostedServer", EmitDefaultValue = false)]
|
||||
private bool? _isHostedServer;
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public int AgentId { get; set; }
|
||||
|
||||
@@ -42,6 +45,21 @@ namespace GitHub.Runner.Common
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string MonitorSocketAddress { get; set; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public bool IsHostedServer
|
||||
{
|
||||
get
|
||||
{
|
||||
// Old runners do not have this property. Hosted runners likely don't have this property either.
|
||||
return _isHostedServer ?? true;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_isHostedServer = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
// Computed property for convenience. Can either return:
|
||||
// 1. If runner was configured at the repo level, returns something like: "myorg/myrepo"
|
||||
@@ -69,6 +87,15 @@ namespace GitHub.Runner.Common
|
||||
return repoOrOrgName;
|
||||
}
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (_isHostedServer.HasValue && _isHostedServer.Value)
|
||||
{
|
||||
_isHostedServer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ServiceLocator(Default = typeof(ConfigurationStore))]
|
||||
@@ -78,8 +105,10 @@ namespace GitHub.Runner.Common
|
||||
bool IsServiceConfigured();
|
||||
bool HasCredentials();
|
||||
CredentialData GetCredentials();
|
||||
CredentialData GetMigratedCredentials();
|
||||
RunnerSettings GetSettings();
|
||||
void SaveCredential(CredentialData credential);
|
||||
void SaveMigratedCredential(CredentialData credential);
|
||||
void SaveSettings(RunnerSettings settings);
|
||||
void DeleteCredential();
|
||||
void DeleteSettings();
|
||||
@@ -90,9 +119,11 @@ namespace GitHub.Runner.Common
|
||||
private string _binPath;
|
||||
private string _configFilePath;
|
||||
private string _credFilePath;
|
||||
private string _migratedCredFilePath;
|
||||
private string _serviceConfigFilePath;
|
||||
|
||||
private CredentialData _creds;
|
||||
private CredentialData _migratedCreds;
|
||||
private RunnerSettings _settings;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
@@ -114,6 +145,9 @@ namespace GitHub.Runner.Common
|
||||
_credFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Credentials);
|
||||
Trace.Info("CredFilePath: {0}", _credFilePath);
|
||||
|
||||
_migratedCredFilePath = hostContext.GetConfigFile(WellKnownConfigFile.MigratedCredentials);
|
||||
Trace.Info("MigratedCredFilePath: {0}", _migratedCredFilePath);
|
||||
|
||||
_serviceConfigFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Service);
|
||||
Trace.Info("ServiceConfigFilePath: {0}", _serviceConfigFilePath);
|
||||
}
|
||||
@@ -123,7 +157,7 @@ namespace GitHub.Runner.Common
|
||||
public bool HasCredentials()
|
||||
{
|
||||
Trace.Info("HasCredentials()");
|
||||
bool credsStored = (new FileInfo(_credFilePath)).Exists;
|
||||
bool credsStored = (new FileInfo(_credFilePath)).Exists || (new FileInfo(_migratedCredFilePath)).Exists;
|
||||
Trace.Info("stored {0}", credsStored);
|
||||
return credsStored;
|
||||
}
|
||||
@@ -154,6 +188,16 @@ namespace GitHub.Runner.Common
|
||||
return _creds;
|
||||
}
|
||||
|
||||
public CredentialData GetMigratedCredentials()
|
||||
{
|
||||
if (_migratedCreds == null && File.Exists(_migratedCredFilePath))
|
||||
{
|
||||
_migratedCreds = IOUtil.LoadObject<CredentialData>(_migratedCredFilePath);
|
||||
}
|
||||
|
||||
return _migratedCreds;
|
||||
}
|
||||
|
||||
public RunnerSettings GetSettings()
|
||||
{
|
||||
if (_settings == null)
|
||||
@@ -188,6 +232,21 @@ namespace GitHub.Runner.Common
|
||||
File.SetAttributes(_credFilePath, File.GetAttributes(_credFilePath) | FileAttributes.Hidden);
|
||||
}
|
||||
|
||||
public void SaveMigratedCredential(CredentialData credential)
|
||||
{
|
||||
Trace.Info("Saving {0} migrated credential @ {1}", credential.Scheme, _migratedCredFilePath);
|
||||
if (File.Exists(_migratedCredFilePath))
|
||||
{
|
||||
// Delete existing credential file first, since the file is hidden and not able to overwrite.
|
||||
Trace.Info("Delete exist runner migrated credential file.");
|
||||
IOUtil.DeleteFile(_migratedCredFilePath);
|
||||
}
|
||||
|
||||
IOUtil.SaveObject(credential, _migratedCredFilePath);
|
||||
Trace.Info("Migrated Credentials Saved.");
|
||||
File.SetAttributes(_migratedCredFilePath, File.GetAttributes(_migratedCredFilePath) | FileAttributes.Hidden);
|
||||
}
|
||||
|
||||
public void SaveSettings(RunnerSettings settings)
|
||||
{
|
||||
Trace.Info("Saving runner settings.");
|
||||
@@ -206,6 +265,7 @@ namespace GitHub.Runner.Common
|
||||
public void DeleteCredential()
|
||||
{
|
||||
IOUtil.Delete(_credFilePath, default(CancellationToken));
|
||||
IOUtil.Delete(_migratedCredFilePath, default(CancellationToken));
|
||||
}
|
||||
|
||||
public void DeleteSettings()
|
||||
|
||||
@@ -19,11 +19,13 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
Runner,
|
||||
Credentials,
|
||||
MigratedCredentials,
|
||||
RSACredentials,
|
||||
Service,
|
||||
CredentialStore,
|
||||
Certificates,
|
||||
Options,
|
||||
SetupInfo,
|
||||
}
|
||||
|
||||
public static class Constants
|
||||
@@ -136,6 +138,12 @@ namespace GitHub.Runner.Common
|
||||
}
|
||||
}
|
||||
|
||||
public static class RunnerEvent
|
||||
{
|
||||
public static readonly string Register = "register";
|
||||
public static readonly string Remove = "remove";
|
||||
}
|
||||
|
||||
public static class Pipeline
|
||||
{
|
||||
public static class Path
|
||||
@@ -162,7 +170,8 @@ namespace GitHub.Runner.Common
|
||||
public static class Path
|
||||
{
|
||||
public static readonly string ActionsDirectory = "_actions";
|
||||
public static readonly string ActionManifestFile = "action.yml";
|
||||
public static readonly string ActionManifestYmlFile = "action.yml";
|
||||
public static readonly string ActionManifestYamlFile = "action.yaml";
|
||||
public static readonly string BinDirectory = "bin";
|
||||
public static readonly string DiagDirectory = "_diag";
|
||||
public static readonly string ExternalsDirectory = "externals";
|
||||
|
||||
@@ -281,6 +281,12 @@ namespace GitHub.Runner.Common
|
||||
".credentials");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.MigratedCredentials:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
".credentials_migrated");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.RSACredentials:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
@@ -316,6 +322,13 @@ namespace GitHub.Runner.Common
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
".options");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.SetupInfo:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
".setup_info");
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected well known config file: '{configFile}'");
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace GitHub.Runner.Common
|
||||
|
||||
private Guid _timelineId;
|
||||
private Guid _timelineRecordId;
|
||||
private string _pageId;
|
||||
private FileStream _pageData;
|
||||
private StreamWriter _pageWriter;
|
||||
private int _byteCount;
|
||||
@@ -40,7 +39,6 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
_totalLines = 0;
|
||||
_pageId = Guid.NewGuid().ToString();
|
||||
_pagesFolder = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Diag), PagingFolder);
|
||||
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
|
||||
Directory.CreateDirectory(_pagesFolder);
|
||||
@@ -102,7 +100,7 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
EndPage();
|
||||
_byteCount = 0;
|
||||
_dataFileName = Path.Combine(_pagesFolder, $"{_pageId}_{++_pageCount}.log");
|
||||
_dataFileName = Path.Combine(_pagesFolder, $"{_timelineId}_{_timelineRecordId}_{++_pageCount}.log");
|
||||
_pageData = new FileStream(_dataFileName, FileMode.CreateNew);
|
||||
_pageWriter = new StreamWriter(_pageData, System.Text.Encoding.UTF8);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ namespace GitHub.Runner.Common
|
||||
|
||||
// agent update
|
||||
Task<TaskAgent> UpdateAgentUpdateStateAsync(int agentPoolId, int agentId, string currentState);
|
||||
|
||||
// runner authorization url
|
||||
Task<string> GetRunnerAuthUrlAsync(int runnerPoolId, int runnerId);
|
||||
Task ReportRunnerAuthUrlErrorAsync(int runnerPoolId, int runnerId, string error);
|
||||
}
|
||||
|
||||
public sealed class RunnerServer : RunnerService, IRunnerServer
|
||||
@@ -334,5 +338,20 @@ namespace GitHub.Runner.Common
|
||||
CheckConnection(RunnerConnectionType.Generic);
|
||||
return _genericTaskAgentClient.UpdateAgentUpdateStateAsync(agentPoolId, agentId, currentState);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
// Runner Auth Url
|
||||
//-----------------------------------------------------------------
|
||||
public Task<string> GetRunnerAuthUrlAsync(int runnerPoolId, int runnerId)
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.MessageQueue);
|
||||
return _messageTaskAgentClient.GetAgentAuthUrlAsync(runnerPoolId, runnerId);
|
||||
}
|
||||
|
||||
public Task ReportRunnerAuthUrlErrorAsync(int runnerPoolId, int runnerId, string error)
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.MessageQueue);
|
||||
return _messageTaskAgentClient.ReportAgentAuthUrlMigrationErrorAsync(runnerPoolId, runnerId, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.Token,
|
||||
description: "Enter runner deletion token:",
|
||||
description: "Enter runner remove token:",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.OAuth;
|
||||
using GitHub.Services.WebApi;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.InteropServices;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
@@ -87,7 +86,6 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
RunnerSettings runnerSettings = new RunnerSettings();
|
||||
|
||||
bool isHostedServer = false;
|
||||
// Loop getting url and creds until you can connect
|
||||
ICredentialProvider credProvider = null;
|
||||
VssCredentials creds = null;
|
||||
@@ -96,8 +94,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
// Get the URL
|
||||
var inputUrl = command.GetUrl();
|
||||
if (!inputUrl.Contains("github.com", StringComparison.OrdinalIgnoreCase) &&
|
||||
!inputUrl.Contains("github.localhost", StringComparison.OrdinalIgnoreCase))
|
||||
if (inputUrl.Contains("codedev.ms", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
runnerSettings.ServerUrl = inputUrl;
|
||||
// Get the credentials
|
||||
@@ -109,7 +106,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
runnerSettings.GitHubUrl = inputUrl;
|
||||
var githubToken = command.GetRunnerRegisterToken();
|
||||
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, githubToken);
|
||||
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, githubToken, Constants.RunnerEvent.Register);
|
||||
runnerSettings.ServerUrl = authResult.TenantUrl;
|
||||
creds = authResult.ToVssCredentials();
|
||||
Trace.Info("cred retrieved via GitHub auth");
|
||||
@@ -118,7 +115,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
try
|
||||
{
|
||||
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
|
||||
isHostedServer = await IsHostedServer(runnerSettings.ServerUrl, creds);
|
||||
runnerSettings.IsHostedServer = runnerSettings.GitHubUrl == null || IsHostedServer(new UriBuilder(runnerSettings.GitHubUrl));
|
||||
|
||||
// Validate can connect.
|
||||
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), creds);
|
||||
@@ -249,14 +246,6 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
UriBuilder configServerUrl = new UriBuilder(runnerSettings.ServerUrl);
|
||||
UriBuilder oauthEndpointUrlBuilder = new UriBuilder(agent.Authorization.AuthorizationUrl);
|
||||
if (!isHostedServer && Uri.Compare(configServerUrl.Uri, oauthEndpointUrlBuilder.Uri, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
oauthEndpointUrlBuilder.Scheme = configServerUrl.Scheme;
|
||||
oauthEndpointUrlBuilder.Host = configServerUrl.Host;
|
||||
oauthEndpointUrlBuilder.Port = configServerUrl.Port;
|
||||
Trace.Info($"Set oauth endpoint url's scheme://host:port component to match runner configure url's scheme://host:port: '{oauthEndpointUrlBuilder.Uri.AbsoluteUri}'.");
|
||||
}
|
||||
|
||||
var credentialData = new CredentialData
|
||||
{
|
||||
Scheme = Constants.Configuration.OAuth,
|
||||
@@ -277,12 +266,15 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
throw new NotSupportedException("Message queue listen OAuth token.");
|
||||
}
|
||||
|
||||
// Testing agent connection, detect any protential connection issue, like local clock skew that cause OAuth token expired.
|
||||
// Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired.
|
||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||
VssCredentials credential = credMgr.LoadCredentials();
|
||||
try
|
||||
{
|
||||
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
|
||||
// ConnectAsync() hits _apis/connectionData which is an anonymous endpoint
|
||||
// Need to hit an authenticate endpoint to trigger OAuth token exchange.
|
||||
await _runnerServer.GetAgentPoolsAsync();
|
||||
_term.WriteSuccessMessage("Runner connection is good");
|
||||
}
|
||||
catch (VssOAuthTokenRequestException ex) when (ex.Message.Contains("Current server time is"))
|
||||
@@ -373,13 +365,12 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
else
|
||||
{
|
||||
var githubToken = command.GetRunnerDeletionToken();
|
||||
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, githubToken);
|
||||
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, githubToken, Constants.RunnerEvent.Remove);
|
||||
creds = authResult.ToVssCredentials();
|
||||
Trace.Info("cred retrieved via GitHub auth");
|
||||
}
|
||||
|
||||
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
|
||||
bool isHostedServer = await IsHostedServer(settings.ServerUrl, creds);
|
||||
await _runnerServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
|
||||
|
||||
var agents = await _runnerServer.GetAgentsAsync(settings.PoolId, settings.AgentName);
|
||||
@@ -496,38 +487,39 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
return agent;
|
||||
}
|
||||
|
||||
private async Task<bool> IsHostedServer(string serverUrl, VssCredentials credentials)
|
||||
private bool IsHostedServer(UriBuilder gitHubUrl)
|
||||
{
|
||||
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
|
||||
var locationServer = HostContext.GetService<ILocationServer>();
|
||||
VssConnection connection = VssUtil.CreateConnection(new Uri(serverUrl), credentials);
|
||||
await locationServer.ConnectAsync(connection);
|
||||
try
|
||||
{
|
||||
var connectionData = await locationServer.GetConnectionDataAsync();
|
||||
Trace.Info($"Server deployment type: {connectionData.DeploymentType}");
|
||||
return connectionData.DeploymentType.HasFlag(DeploymentFlags.Hosted);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Since the DeploymentType is Enum, deserialization exception means there is a new Enum member been added.
|
||||
// It's more likely to be Hosted since OnPremises is always behind and customer can update their agent if are on-prem
|
||||
Trace.Error(ex);
|
||||
return true;
|
||||
}
|
||||
return string.Equals(gitHubUrl.Host, "github.com", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(gitHubUrl.Host, "www.github.com", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(gitHubUrl.Host, "github.localhost", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task<GitHubAuthResult> GetTenantCredential(string githubUrl, string githubToken)
|
||||
private async Task<GitHubAuthResult> GetTenantCredential(string githubUrl, string githubToken, string runnerEvent)
|
||||
{
|
||||
var gitHubUrl = new UriBuilder(githubUrl);
|
||||
var githubApiUrl = $"https://api.{gitHubUrl.Host}/repos/{gitHubUrl.Path.Trim('/')}/actions-runners/registration";
|
||||
var githubApiUrl = "";
|
||||
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
||||
if (IsHostedServer(gitHubUrlBuilder))
|
||||
{
|
||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/actions/runner-registration";
|
||||
}
|
||||
else
|
||||
{
|
||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/actions/runner-registration";
|
||||
}
|
||||
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(HostContext.UserAgent);
|
||||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.shuri-preview+json"));
|
||||
var response = await httpClient.PostAsync(githubApiUrl, new StringContent("", null, "application/json"));
|
||||
|
||||
var bodyObject = new Dictionary<string, string>()
|
||||
{
|
||||
{"url", githubUrl},
|
||||
{"runner_event", runnerEvent}
|
||||
};
|
||||
|
||||
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"));
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
public interface ICredentialManager : IRunnerService
|
||||
{
|
||||
ICredentialProvider GetCredentialProvider(string credType);
|
||||
VssCredentials LoadCredentials();
|
||||
VssCredentials LoadCredentials(bool preferMigrated = true);
|
||||
}
|
||||
|
||||
public class CredentialManager : RunnerService, ICredentialManager
|
||||
@@ -40,7 +40,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
return creds;
|
||||
}
|
||||
|
||||
public VssCredentials LoadCredentials()
|
||||
public VssCredentials LoadCredentials(bool preferMigrated = true)
|
||||
{
|
||||
IConfigurationStore store = HostContext.GetService<IConfigurationStore>();
|
||||
|
||||
@@ -50,6 +50,16 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
|
||||
CredentialData credData = store.GetCredentials();
|
||||
|
||||
if (preferMigrated)
|
||||
{
|
||||
var migratedCred = store.GetMigratedCredentials();
|
||||
if (migratedCred != null)
|
||||
{
|
||||
credData = migratedCred;
|
||||
}
|
||||
}
|
||||
|
||||
ICredentialProvider credProv = GetCredentialProvider(credData.Scheme);
|
||||
credProv.CredentialData = credData;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.OAuth;
|
||||
@@ -29,7 +28,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
var authorizationUrl = this.CredentialData.Data.GetValueOrDefault("authorizationUrl", null);
|
||||
|
||||
// For back compat with .credential file that doesn't has 'oauthEndpointUrl' section
|
||||
var oathEndpointUrl = this.CredentialData.Data.GetValueOrDefault("oauthEndpointUrl", authorizationUrl);
|
||||
var oauthEndpointUrl = this.CredentialData.Data.GetValueOrDefault("oauthEndpointUrl", authorizationUrl);
|
||||
|
||||
ArgUtil.NotNullOrEmpty(clientId, nameof(clientId));
|
||||
ArgUtil.NotNullOrEmpty(authorizationUrl, nameof(authorizationUrl));
|
||||
@@ -39,7 +38,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
var keyManager = context.GetService<IRSAKeyManager>();
|
||||
var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey());
|
||||
var clientCredential = new VssOAuthJwtBearerClientCredential(clientId, authorizationUrl, signingCredentials);
|
||||
var agentCredential = new VssOAuthCredential(new Uri(oathEndpointUrl, UriKind.Absolute), VssOAuthGrant.ClientCredentials, clientCredential);
|
||||
var agentCredential = new VssOAuthCredential(new Uri(oauthEndpointUrl, UriKind.Absolute), VssOAuthGrant.ClientCredentials, clientCredential);
|
||||
|
||||
// Construct a credentials cache with a single OAuth credential for communication. The windows credential
|
||||
// is explicitly set to null to ensure we never do that negotiation.
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace GitHub.Runner.Listener
|
||||
[ServiceLocator(Default = typeof(JobDispatcher))]
|
||||
public interface IJobDispatcher : IRunnerService
|
||||
{
|
||||
bool Busy { get; }
|
||||
TaskCompletionSource<bool> RunOnceJobCompleted { get; }
|
||||
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
|
||||
bool Cancel(JobCancelMessage message);
|
||||
@@ -69,6 +70,8 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
public TaskCompletionSource<bool> RunOnceJobCompleted => _runOnceJobCompleted;
|
||||
|
||||
public bool Busy { get; private set; }
|
||||
|
||||
public void Run(Pipelines.AgentJobRequestMessage jobRequestMessage, bool runOnce = false)
|
||||
{
|
||||
Trace.Info($"Job request {jobRequestMessage.RequestId} for plan {jobRequestMessage.Plan.PlanId} job {jobRequestMessage.JobId} received.");
|
||||
@@ -247,7 +250,7 @@ namespace GitHub.Runner.Listener
|
||||
Task completedTask = await Task.WhenAny(jobDispatch.WorkerDispatch, Task.Delay(TimeSpan.FromSeconds(45)));
|
||||
if (completedTask != jobDispatch.WorkerDispatch)
|
||||
{
|
||||
// at this point, the job exectuion might encounter some dead lock and even not able to be canclled.
|
||||
// at this point, the job execution might encounter some dead lock and even not able to be cancelled.
|
||||
// no need to localize the exception string should never happen.
|
||||
throw new InvalidOperationException($"Job dispatch process for {jobDispatch.JobId} has encountered unexpected error, the dispatch task is not able to be canceled within 45 seconds.");
|
||||
}
|
||||
@@ -295,6 +298,9 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
|
||||
private async Task RunAsync(Pipelines.AgentJobRequestMessage message, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||
{
|
||||
Busy = true;
|
||||
try
|
||||
{
|
||||
if (previousJobDispatch != null)
|
||||
{
|
||||
@@ -568,6 +574,10 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
Trace.Info("worker process has been killed.");
|
||||
}
|
||||
|
||||
// When worker doesn't exit within cancel timeout, the runner will kill the worker process and worker won't finish upload job logs.
|
||||
// The runner will try to upload these logs at this time.
|
||||
await TryUploadUnfinishedLogs(message);
|
||||
}
|
||||
|
||||
Trace.Info($"finish job request for job {message.JobId} with result: {resultOnAbandonOrCancel}");
|
||||
@@ -591,6 +601,11 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RenewJobRequestAsync(int poolId, long requestId, Guid lockToken, TaskCompletionSource<int> firstJobRequestRenewed, CancellationToken token)
|
||||
{
|
||||
@@ -712,6 +727,121 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
// Best effort upload any logs for this job.
|
||||
private async Task TryUploadUnfinishedLogs(Pipelines.AgentJobRequestMessage message)
|
||||
{
|
||||
Trace.Entering();
|
||||
|
||||
var logFolder = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), PagingLogger.PagingFolder);
|
||||
if (!Directory.Exists(logFolder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var logs = Directory.GetFiles(logFolder);
|
||||
if (logs.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection));
|
||||
ArgUtil.NotNull(systemConnection, nameof(systemConnection));
|
||||
|
||||
var jobServer = HostContext.GetService<IJobServer>();
|
||||
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
||||
VssConnection jobConnection = VssUtil.CreateConnection(systemConnection.Url, jobServerCredential);
|
||||
|
||||
await jobServer.ConnectAsync(jobConnection);
|
||||
|
||||
var timeline = await jobServer.GetTimelineAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, CancellationToken.None);
|
||||
|
||||
var updatedRecords = new List<TimelineRecord>();
|
||||
var logPages = new Dictionary<Guid, Dictionary<int, string>>();
|
||||
var logRecords = new Dictionary<Guid, TimelineRecord>();
|
||||
foreach (var log in logs)
|
||||
{
|
||||
var logName = Path.GetFileNameWithoutExtension(log);
|
||||
var logNameParts = logName.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (logNameParts.Length != 3)
|
||||
{
|
||||
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
||||
continue;
|
||||
}
|
||||
var logPageSeperator = logName.IndexOf('_');
|
||||
var logRecordId = Guid.Empty;
|
||||
var pageNumber = 0;
|
||||
|
||||
if (!Guid.TryParse(logNameParts[0], out Guid timelineId) || timelineId != timeline.Id)
|
||||
{
|
||||
Trace.Warning($"log file '{log}' is not belongs to current job");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(logNameParts[1], out logRecordId))
|
||||
{
|
||||
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!int.TryParse(logNameParts[2], out pageNumber))
|
||||
{
|
||||
Trace.Warning($"log file '{log}' doesn't follow naming convension 'GUID_GUID_INT'.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var record = timeline.Records.FirstOrDefault(x => x.Id == logRecordId);
|
||||
if (record != null)
|
||||
{
|
||||
if (!logPages.ContainsKey(record.Id))
|
||||
{
|
||||
logPages[record.Id] = new Dictionary<int, string>();
|
||||
logRecords[record.Id] = record;
|
||||
}
|
||||
|
||||
logPages[record.Id][pageNumber] = log;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var pages in logPages)
|
||||
{
|
||||
var record = logRecords[pages.Key];
|
||||
if (record.Log == null)
|
||||
{
|
||||
// Create the log
|
||||
record.Log = await jobServer.CreateLogAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, new TaskLog(String.Format(@"logs\{0:D}", record.Id)), default(CancellationToken));
|
||||
|
||||
// Need to post timeline record updates to reflect the log creation
|
||||
updatedRecords.Add(record.Clone());
|
||||
}
|
||||
|
||||
for (var i = 1; i <= pages.Value.Count; i++)
|
||||
{
|
||||
var logFile = pages.Value[i];
|
||||
// Upload the contents
|
||||
using (FileStream fs = File.Open(logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
var logUploaded = await jobServer.AppendLogContentAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, record.Log.Id, fs, default(CancellationToken));
|
||||
}
|
||||
|
||||
Trace.Info($"Uploaded unfinished log '{logFile}' for current job.");
|
||||
IOUtil.DeleteFile(logFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedRecords.Count > 0)
|
||||
{
|
||||
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, updatedRecords, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore any error during log upload since it's best effort
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We need send detailInfo back to DT in order to add an issue for the job
|
||||
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, Guid lockToken, TaskResult result, string detailInfo = null)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,10 @@ using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.WebApi;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Test")]
|
||||
namespace GitHub.Runner.Listener
|
||||
{
|
||||
[ServiceLocator(Default = typeof(MessageListener))]
|
||||
@@ -32,18 +35,30 @@ namespace GitHub.Runner.Listener
|
||||
private ITerminal _term;
|
||||
private IRunnerServer _runnerServer;
|
||||
private TaskAgentSession _session;
|
||||
private ICredentialManager _credMgr;
|
||||
private IConfigurationStore _configStore;
|
||||
private TimeSpan _getNextMessageRetryInterval;
|
||||
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
|
||||
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
|
||||
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
|
||||
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new Dictionary<string, int>();
|
||||
|
||||
// Whether load credentials from .credentials_migrated file
|
||||
internal bool _useMigratedCredentials;
|
||||
|
||||
// need to check auth url if there is only .credentials and auth schema is OAuth
|
||||
internal bool _needToCheckAuthorizationUrlUpdate;
|
||||
internal Task<VssCredentials> _authorizationUrlMigrationBackgroundTask;
|
||||
internal Task _authorizationUrlRollbackReattemptDelayBackgroundTask;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
|
||||
_term = HostContext.GetService<ITerminal>();
|
||||
_runnerServer = HostContext.GetService<IRunnerServer>();
|
||||
_credMgr = HostContext.GetService<ICredentialManager>();
|
||||
_configStore = HostContext.GetService<IConfigurationStore>();
|
||||
}
|
||||
|
||||
public async Task<Boolean> CreateSessionAsync(CancellationToken token)
|
||||
@@ -58,8 +73,8 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
// Create connection.
|
||||
Trace.Info("Loading Credentials");
|
||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||
VssCredentials creds = credMgr.LoadCredentials();
|
||||
_useMigratedCredentials = !StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_SPSAUTHURL"));
|
||||
VssCredentials creds = _credMgr.LoadCredentials(_useMigratedCredentials);
|
||||
|
||||
var agent = new TaskAgentReference
|
||||
{
|
||||
@@ -74,6 +89,17 @@ namespace GitHub.Runner.Listener
|
||||
string errorMessage = string.Empty;
|
||||
bool encounteringError = false;
|
||||
|
||||
var originalCreds = _configStore.GetCredentials();
|
||||
var migratedCreds = _configStore.GetMigratedCredentials();
|
||||
if (migratedCreds == null)
|
||||
{
|
||||
_useMigratedCredentials = false;
|
||||
if (originalCreds.Scheme == Constants.Configuration.OAuth)
|
||||
{
|
||||
_needToCheckAuthorizationUrlUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -101,6 +127,12 @@ namespace GitHub.Runner.Listener
|
||||
encounteringError = false;
|
||||
}
|
||||
|
||||
if (_needToCheckAuthorizationUrlUpdate)
|
||||
{
|
||||
// start background task try to get new authorization url
|
||||
_authorizationUrlMigrationBackgroundTask = GetNewOAuthAuthorizationSetting(token);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
@@ -119,10 +151,23 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Error(ex);
|
||||
|
||||
if (!IsSessionCreationExceptionRetriable(ex))
|
||||
{
|
||||
if (_useMigratedCredentials)
|
||||
{
|
||||
// migrated credentials might cause lose permission during permission check,
|
||||
// we will force to use original credential and try again
|
||||
_useMigratedCredentials = false;
|
||||
var reattemptBackoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromHours(24), TimeSpan.FromHours(36));
|
||||
_authorizationUrlRollbackReattemptDelayBackgroundTask = HostContext.Delay(reattemptBackoff, token); // retry migrated creds in 24-36 hours.
|
||||
creds = _credMgr.LoadCredentials(false);
|
||||
Trace.Error("Fallback to original credentials and try again.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_term.WriteError($"Failed to create session. {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!encounteringError) //print the message only on the first error
|
||||
{
|
||||
@@ -182,6 +227,51 @@ namespace GitHub.Runner.Listener
|
||||
encounteringError = false;
|
||||
continuousError = 0;
|
||||
}
|
||||
|
||||
if (_needToCheckAuthorizationUrlUpdate &&
|
||||
_authorizationUrlMigrationBackgroundTask?.IsCompleted == true)
|
||||
{
|
||||
if (HostContext.GetService<IJobDispatcher>().Busy ||
|
||||
HostContext.GetService<ISelfUpdater>().Busy)
|
||||
{
|
||||
Trace.Info("Job or runner updates in progress, update credentials next time.");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var newCred = await _authorizationUrlMigrationBackgroundTask;
|
||||
await _runnerServer.ConnectAsync(new Uri(_settings.ServerUrl), newCred);
|
||||
Trace.Info("Updated connection to use migrated credential for next GetMessage call.");
|
||||
_useMigratedCredentials = true;
|
||||
_authorizationUrlMigrationBackgroundTask = null;
|
||||
_needToCheckAuthorizationUrlUpdate = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to refresh connection with new authorization url.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_authorizationUrlRollbackReattemptDelayBackgroundTask?.IsCompleted == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
// we rolled back to use original creds about 2 days before, now it's a good time to try migrated creds again.
|
||||
Trace.Info("Re-attempt to use migrated credential");
|
||||
var migratedCreds = _credMgr.LoadCredentials();
|
||||
await _runnerServer.ConnectAsync(new Uri(_settings.ServerUrl), migratedCreds);
|
||||
_useMigratedCredentials = true;
|
||||
_authorizationUrlRollbackReattemptDelayBackgroundTask = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to refresh connection with new authorization url on rollback reattempt.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -204,9 +294,23 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Info($"{nameof(TaskAgentSessionExpiredException)} received, recovered by recreate session.");
|
||||
}
|
||||
else if (!IsGetNextMessageExceptionRetriable(ex))
|
||||
{
|
||||
if (_useMigratedCredentials)
|
||||
{
|
||||
// migrated credentials might cause lose permission during permission check,
|
||||
// we will force to use original credential and try again
|
||||
_useMigratedCredentials = false;
|
||||
var reattemptBackoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromHours(24), TimeSpan.FromHours(36));
|
||||
_authorizationUrlRollbackReattemptDelayBackgroundTask = HostContext.Delay(reattemptBackoff, token); // retry migrated creds in 24-36 hours.
|
||||
var originalCreds = _credMgr.LoadCredentials(false);
|
||||
await _runnerServer.ConnectAsync(new Uri(_settings.ServerUrl), originalCreds);
|
||||
Trace.Error("Fallback to original credentials and try again.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
continuousError++;
|
||||
@@ -397,5 +501,80 @@ namespace GitHub.Runner.Listener
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<VssCredentials> GetNewOAuthAuthorizationSetting(CancellationToken token)
|
||||
{
|
||||
Trace.Info("Start checking oauth authorization url update.");
|
||||
while (true)
|
||||
{
|
||||
var backoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(45));
|
||||
await HostContext.Delay(backoff, token);
|
||||
|
||||
try
|
||||
{
|
||||
var migratedAuthorizationUrl = await _runnerServer.GetRunnerAuthUrlAsync(_settings.PoolId, _settings.AgentId);
|
||||
if (!string.IsNullOrEmpty(migratedAuthorizationUrl))
|
||||
{
|
||||
var credData = _configStore.GetCredentials();
|
||||
var clientId = credData.Data.GetValueOrDefault("clientId", null);
|
||||
var currentAuthorizationUrl = credData.Data.GetValueOrDefault("authorizationUrl", null);
|
||||
Trace.Info($"Current authorization url: {currentAuthorizationUrl}, new authorization url: {migratedAuthorizationUrl}");
|
||||
|
||||
if (string.Equals(currentAuthorizationUrl, migratedAuthorizationUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// We don't need to update credentials.
|
||||
Trace.Info("No needs to update authorization url");
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(-1), token);
|
||||
}
|
||||
|
||||
var keyManager = HostContext.GetService<IRSAKeyManager>();
|
||||
var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey());
|
||||
|
||||
var migratedClientCredential = new VssOAuthJwtBearerClientCredential(clientId, migratedAuthorizationUrl, signingCredentials);
|
||||
var migratedRunnerCredential = new VssOAuthCredential(new Uri(migratedAuthorizationUrl, UriKind.Absolute), VssOAuthGrant.ClientCredentials, migratedClientCredential);
|
||||
|
||||
Trace.Info("Try connect service with Token Service OAuth endpoint.");
|
||||
var runnerServer = HostContext.CreateService<IRunnerServer>();
|
||||
await runnerServer.ConnectAsync(new Uri(_settings.ServerUrl), migratedRunnerCredential);
|
||||
await runnerServer.GetAgentPoolsAsync();
|
||||
Trace.Info($"Successfully connected service with new authorization url.");
|
||||
|
||||
var migratedCredData = new CredentialData
|
||||
{
|
||||
Scheme = Constants.Configuration.OAuth,
|
||||
Data =
|
||||
{
|
||||
{ "clientId", clientId },
|
||||
{ "authorizationUrl", migratedAuthorizationUrl },
|
||||
{ "oauthEndpointUrl", migratedAuthorizationUrl },
|
||||
},
|
||||
};
|
||||
|
||||
_configStore.SaveMigratedCredential(migratedCredData);
|
||||
return migratedRunnerCredential;
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Verbose("No authorization url updates");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to get/test new authorization url.");
|
||||
Trace.Error(ex);
|
||||
|
||||
try
|
||||
{
|
||||
await _runnerServer.ReportRunnerAuthUrlErrorAsync(_settings.PoolId, _settings.AgentId, ex.ToString());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// best effort
|
||||
Trace.Error("Fail to report the migration error");
|
||||
Trace.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,16 +451,38 @@ namespace GitHub.Runner.Listener
|
||||
ext = "sh";
|
||||
#endif
|
||||
_term.WriteLine($@"
|
||||
Commands:,
|
||||
Commands:
|
||||
.{separator}config.{ext} Configures the runner
|
||||
.{separator}config.{ext} remove Unconfigures the runner
|
||||
.{separator}run.{ext} Runs the runner interactively. Does not require any options.
|
||||
|
||||
Options:
|
||||
--help Prints the help for each command
|
||||
--version Prints the runner version
|
||||
--commit Prints the runner commit
|
||||
--help Prints the help for each command
|
||||
");
|
||||
|
||||
Config Options:
|
||||
--unattended Disable interactive prompts for missing arguments. Defaults will be used for missing options
|
||||
--url string Repository to add the runner to. Required if unattended
|
||||
--token string Registration token. Required if unattended
|
||||
--name string Name of the runner to configure (default {Environment.MachineName ?? "myrunner"})
|
||||
--work string Relative runner work directory (default {Constants.Path.WorkDirectory})
|
||||
--replace Replace any existing runner with the same name (default false)");
|
||||
#if OS_WINDOWS
|
||||
_term.WriteLine($@" --runasservice Run the runner as a service");
|
||||
_term.WriteLine($@" --windowslogonaccount string Account to run the service as. Requires runasservice");
|
||||
_term.WriteLine($@" --windowslogonpassword string Password for the service account. Requires runasservice");
|
||||
#endif
|
||||
_term.WriteLine($@"
|
||||
Examples:
|
||||
Configure a runner non-interactively:
|
||||
.{separator}config.{ext} --unattended --url <url> --token <token>
|
||||
Configure a runner non-interactively, replacing any existing runner with the same name:
|
||||
.{separator}config.{ext} --unattended --url <url> --token <token> --replace [--name <name>]");
|
||||
#if OS_WINDOWS
|
||||
_term.WriteLine($@" Configure a runner to run as a service:");
|
||||
_term.WriteLine($@" .{separator}config.{ext} --url <url> --token <token> --runasservice");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace GitHub.Runner.Listener
|
||||
[ServiceLocator(Default = typeof(SelfUpdater))]
|
||||
public interface ISelfUpdater : IRunnerService
|
||||
{
|
||||
bool Busy { get; }
|
||||
Task<bool> SelfUpdate(AgentRefreshMessage updateMessage, IJobDispatcher jobDispatcher, bool restartInteractiveRunner, CancellationToken token);
|
||||
}
|
||||
|
||||
@@ -31,6 +32,8 @@ namespace GitHub.Runner.Listener
|
||||
private int _poolId;
|
||||
private int _agentId;
|
||||
|
||||
public bool Busy { get; private set; }
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
@@ -44,6 +47,9 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
|
||||
public async Task<bool> SelfUpdate(AgentRefreshMessage updateMessage, IJobDispatcher jobDispatcher, bool restartInteractiveRunner, CancellationToken token)
|
||||
{
|
||||
Busy = true;
|
||||
try
|
||||
{
|
||||
if (!await UpdateNeeded(updateMessage.TargetVersion, token))
|
||||
{
|
||||
@@ -92,6 +98,11 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> UpdateNeeded(string targetVersion, CancellationToken token)
|
||||
{
|
||||
|
||||
@@ -271,6 +271,14 @@ namespace GitHub.Runner.Sdk
|
||||
// Indicate GitHub Actions process.
|
||||
_proc.StartInfo.Environment["GITHUB_ACTIONS"] = "true";
|
||||
|
||||
// Set CI=true when no one else already set it.
|
||||
// CI=true is common set in most CI provider in GitHub
|
||||
if (!_proc.StartInfo.Environment.ContainsKey("CI") &&
|
||||
Environment.GetEnvironmentVariable("CI") == null)
|
||||
{
|
||||
_proc.StartInfo.Environment["CI"] = "true";
|
||||
}
|
||||
|
||||
// Hook up the events.
|
||||
_proc.EnableRaisingEvents = true;
|
||||
_proc.Exited += ProcessExitedHandler;
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace GitHub.Runner.Sdk
|
||||
private string _httpsProxyAddress;
|
||||
private string _httpsProxyUsername;
|
||||
private string _httpsProxyPassword;
|
||||
private string _noProxyString;
|
||||
|
||||
private readonly List<ByPassInfo> _noProxyList = new List<ByPassInfo>();
|
||||
private readonly HashSet<string> _noProxyUnique = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -33,6 +34,7 @@ namespace GitHub.Runner.Sdk
|
||||
public string HttpsProxyAddress => _httpsProxyAddress;
|
||||
public string HttpsProxyUsername => _httpsProxyUsername;
|
||||
public string HttpsProxyPassword => _httpsProxyPassword;
|
||||
public string NoProxyString => _noProxyString;
|
||||
|
||||
public List<ByPassInfo> NoProxyList => _noProxyList;
|
||||
|
||||
@@ -71,6 +73,10 @@ namespace GitHub.Runner.Sdk
|
||||
{
|
||||
_httpProxyAddress = proxyHttpUri.AbsoluteUri;
|
||||
|
||||
// Set both environment variables since there are tools support both casing (curl, wget) and tools support only one casing (docker)
|
||||
Environment.SetEnvironmentVariable("HTTP_PROXY", _httpProxyAddress);
|
||||
Environment.SetEnvironmentVariable("http_proxy", _httpProxyAddress);
|
||||
|
||||
// the proxy url looks like http://[user:pass@]127.0.0.1:8888
|
||||
var userInfo = Uri.UnescapeDataString(proxyHttpUri.UserInfo).Split(':', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (userInfo.Length == 2)
|
||||
@@ -97,6 +103,10 @@ namespace GitHub.Runner.Sdk
|
||||
{
|
||||
_httpsProxyAddress = proxyHttpsUri.AbsoluteUri;
|
||||
|
||||
// Set both environment variables since there are tools support both casing (curl, wget) and tools support only one casing (docker)
|
||||
Environment.SetEnvironmentVariable("HTTPS_PROXY", _httpsProxyAddress);
|
||||
Environment.SetEnvironmentVariable("https_proxy", _httpsProxyAddress);
|
||||
|
||||
// the proxy url looks like http://[user:pass@]127.0.0.1:8888
|
||||
var userInfo = Uri.UnescapeDataString(proxyHttpsUri.UserInfo).Split(':', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (userInfo.Length == 2)
|
||||
@@ -121,6 +131,12 @@ namespace GitHub.Runner.Sdk
|
||||
|
||||
if (!string.IsNullOrEmpty(noProxyList))
|
||||
{
|
||||
_noProxyString = noProxyList;
|
||||
|
||||
// Set both environment variables since there are tools support both casing (curl, wget) and tools support only one casing (docker)
|
||||
Environment.SetEnvironmentVariable("NO_PROXY", noProxyList);
|
||||
Environment.SetEnvironmentVariable("no_proxy", noProxyList);
|
||||
|
||||
var noProxyListSplit = noProxyList.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (string noProxy in noProxyListSplit)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -15,14 +16,14 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
void EnablePluginInternalCommand();
|
||||
void DisablePluginInternalCommand();
|
||||
bool TryProcessCommand(IExecutionContext context, string input);
|
||||
bool TryProcessCommand(IExecutionContext context, string input, ContainerInfo container);
|
||||
}
|
||||
|
||||
public sealed class ActionCommandManager : RunnerService, IActionCommandManager
|
||||
{
|
||||
private const string _stopCommand = "stop-commands";
|
||||
private readonly Dictionary<string, IActionCommandExtension> _commandExtensions = new Dictionary<string, IActionCommandExtension>(StringComparer.OrdinalIgnoreCase);
|
||||
private HashSet<string> _registeredCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _registeredCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly object _commandSerializeLock = new object();
|
||||
private bool _stopProcessCommand = false;
|
||||
private string _stopToken = null;
|
||||
@@ -58,7 +59,7 @@ namespace GitHub.Runner.Worker
|
||||
_registeredCommands.Remove("internal-set-repo-path");
|
||||
}
|
||||
|
||||
public bool TryProcessCommand(IExecutionContext context, string input)
|
||||
public bool TryProcessCommand(IExecutionContext context, string input, ContainerInfo container)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
@@ -114,7 +115,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
try
|
||||
{
|
||||
extension.ProcessCommand(context, input, actionCommand);
|
||||
extension.ProcessCommand(context, input, actionCommand, container);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -140,7 +141,7 @@ namespace GitHub.Runner.Worker
|
||||
string Command { get; }
|
||||
bool OmitEcho { get; }
|
||||
|
||||
void ProcessCommand(IExecutionContext context, string line, ActionCommand command);
|
||||
void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container);
|
||||
}
|
||||
|
||||
public sealed class InternalPluginSetRepoPathCommandExtension : RunnerService, IActionCommandExtension
|
||||
@@ -150,7 +151,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
if (!command.Properties.TryGetValue(SetRepoPathCommandProperties.repoFullName, out string repoFullName) || string.IsNullOrEmpty(repoFullName))
|
||||
{
|
||||
@@ -180,7 +181,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
if (!command.Properties.TryGetValue(SetEnvCommandProperties.Name, out string envName) || string.IsNullOrEmpty(envName))
|
||||
{
|
||||
@@ -205,7 +206,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
|
||||
{
|
||||
@@ -229,7 +230,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
|
||||
{
|
||||
@@ -253,7 +254,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(command.Data))
|
||||
{
|
||||
@@ -279,7 +280,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
ArgUtil.NotNullOrEmpty(command.Data, "path");
|
||||
context.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
|
||||
@@ -294,7 +295,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
var file = command.Data;
|
||||
|
||||
@@ -306,9 +307,9 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
// Translate file path back from container path
|
||||
if (context.Container != null)
|
||||
if (container != null)
|
||||
{
|
||||
file = context.Container.TranslateToHostPath(file);
|
||||
file = container.TranslateToHostPath(file);
|
||||
}
|
||||
|
||||
// Root the path
|
||||
@@ -341,7 +342,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
command.Properties.TryGetValue(RemoveMatcherCommandProperties.Owner, out string owner);
|
||||
var file = command.Data;
|
||||
@@ -369,9 +370,9 @@ namespace GitHub.Runner.Worker
|
||||
else
|
||||
{
|
||||
// Translate file path back from container path
|
||||
if (context.Container != null)
|
||||
if (container != null)
|
||||
{
|
||||
file = context.Container.TranslateToHostPath(file);
|
||||
file = container.TranslateToHostPath(file);
|
||||
}
|
||||
|
||||
// Root the path
|
||||
@@ -409,7 +410,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
context.Debug(command.Data);
|
||||
}
|
||||
@@ -437,7 +438,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
command.Properties.TryGetValue(IssueCommandProperties.File, out string file);
|
||||
command.Properties.TryGetValue(IssueCommandProperties.Line, out string line);
|
||||
@@ -454,10 +455,10 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
issue.Category = "Code";
|
||||
|
||||
if (context.Container != null)
|
||||
if (container != null)
|
||||
{
|
||||
// Translate file path back from container path
|
||||
file = context.Container.TranslateToHostPath(file);
|
||||
file = container.TranslateToHostPath(file);
|
||||
command.Properties[IssueCommandProperties.File] = file;
|
||||
}
|
||||
|
||||
@@ -517,7 +518,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
var data = this is GroupCommandExtension ? command.Data : string.Empty;
|
||||
context.Output($"##[{Command}]{data}");
|
||||
@@ -531,7 +532,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
|
||||
{
|
||||
ArgUtil.NotNullOrEmpty(command.Data, "value");
|
||||
|
||||
|
||||
@@ -51,15 +51,21 @@ namespace GitHub.Runner.Worker
|
||||
List<JobExtensionRunner> containerSetupSteps = new List<JobExtensionRunner>();
|
||||
IEnumerable<Pipelines.ActionStep> actions = steps.OfType<Pipelines.ActionStep>();
|
||||
|
||||
// TODO: Depreciate the PREVIEW_ACTION_TOKEN
|
||||
// TODO: Deprecate the PREVIEW_ACTION_TOKEN
|
||||
// Log even if we aren't using it to ensure users know.
|
||||
if (!string.IsNullOrEmpty(executionContext.Variables.Get("PREVIEW_ACTION_TOKEN")))
|
||||
{
|
||||
executionContext.Warning("The 'PREVIEW_ACTION_TOKEN' secret is depreciated. Please remove it from the repository's secrets");
|
||||
executionContext.Warning("The 'PREVIEW_ACTION_TOKEN' secret is deprecated. Please remove it from the repository's secrets");
|
||||
}
|
||||
|
||||
// Clear the cache (local runner)
|
||||
// Clear the cache (for self-hosted runners)
|
||||
// Note, temporarily avoid this step for the on-premises product, to avoid rate limiting.
|
||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
||||
var isHostedServer = configurationStore.GetSettings().IsHostedServer;
|
||||
if (isHostedServer)
|
||||
{
|
||||
IOUtil.DeleteDirectory(HostContext.GetDirectory(WellKnownDirectory.Actions), executionContext.CancellationToken);
|
||||
}
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
@@ -198,14 +204,21 @@ namespace GitHub.Runner.Worker
|
||||
Trace.Info($"Load action that reference repository from '{actionDirectory}'");
|
||||
definition.Directory = actionDirectory;
|
||||
|
||||
string manifestFile = Path.Combine(actionDirectory, "action.yml");
|
||||
string manifestFile = Path.Combine(actionDirectory, Constants.Path.ActionManifestYmlFile);
|
||||
string manifestFileYaml = Path.Combine(actionDirectory, Constants.Path.ActionManifestYamlFile);
|
||||
string dockerFile = Path.Combine(actionDirectory, "Dockerfile");
|
||||
string dockerFileLowerCase = Path.Combine(actionDirectory, "dockerfile");
|
||||
if (File.Exists(manifestFile))
|
||||
if (File.Exists(manifestFile) || File.Exists(manifestFileYaml))
|
||||
{
|
||||
var manifestManager = HostContext.GetService<IActionManifestManager>();
|
||||
if (File.Exists(manifestFile))
|
||||
{
|
||||
definition.Data = manifestManager.Load(executionContext, manifestFile);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
definition.Data = manifestManager.Load(executionContext, manifestFileYaml);
|
||||
}
|
||||
Trace.Verbose($"Action friendly name: '{definition.Data.Name}'");
|
||||
Trace.Verbose($"Action description: '{definition.Data.Description}'");
|
||||
|
||||
@@ -314,7 +327,7 @@ namespace GitHub.Runner.Worker
|
||||
else
|
||||
{
|
||||
var fullPath = IOUtil.ResolvePath(actionDirectory, "."); // resolve full path without access filesystem.
|
||||
throw new NotSupportedException($"Can't find 'action.yml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?");
|
||||
throw new NotSupportedException($"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?");
|
||||
}
|
||||
}
|
||||
else if (action.Reference.Type == Pipelines.ActionSourceType.Script)
|
||||
@@ -441,7 +454,8 @@ namespace GitHub.Runner.Worker
|
||||
ArgUtil.NotNullOrEmpty(repositoryReference.Ref, nameof(repositoryReference.Ref));
|
||||
|
||||
string destDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repositoryReference.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repositoryReference.Ref);
|
||||
if (File.Exists(destDirectory + ".completed"))
|
||||
string watermarkFile = destDirectory + ".completed";
|
||||
if (File.Exists(watermarkFile))
|
||||
{
|
||||
executionContext.Debug($"Action '{repositoryReference.Name}@{repositoryReference.Ref}' already downloaded at '{destDirectory}'.");
|
||||
return;
|
||||
@@ -490,11 +504,15 @@ namespace GitHub.Runner.Worker
|
||||
using (FileStream fs = new FileStream(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: _defaultFileStreamBufferSize, useAsync: true))
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
||||
var isHostedServer = configurationStore.GetSettings().IsHostedServer;
|
||||
if (isHostedServer)
|
||||
{
|
||||
var authToken = Environment.GetEnvironmentVariable("_GITHUB_ACTION_TOKEN");
|
||||
if (string.IsNullOrEmpty(authToken))
|
||||
{
|
||||
// TODO: Depreciate the PREVIEW_ACTION_TOKEN
|
||||
// TODO: Deprecate the PREVIEW_ACTION_TOKEN
|
||||
authToken = executionContext.Variables.Get("PREVIEW_ACTION_TOKEN");
|
||||
}
|
||||
|
||||
@@ -510,6 +528,11 @@ namespace GitHub.Runner.Worker
|
||||
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{accessToken}"));
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodingToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Intentionally empty. Temporary for GHES alpha release, download from dotcom unauthenticated.
|
||||
}
|
||||
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(HostContext.UserAgent);
|
||||
using (var result = await httpClient.GetStreamAsync(archiveLink))
|
||||
@@ -603,7 +626,7 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
Trace.Verbose("Create watermark file indicate action download succeed.");
|
||||
File.WriteAllText(destDirectory + ".completed", DateTime.UtcNow.ToString());
|
||||
File.WriteAllText(watermarkFile, DateTime.UtcNow.ToString());
|
||||
|
||||
executionContext.Debug($"Archive '{archiveFile}' has been unzipped into '{destDirectory}'.");
|
||||
Trace.Info("Finished getting action repository.");
|
||||
@@ -655,12 +678,21 @@ namespace GitHub.Runner.Worker
|
||||
// find the docker file or action.yml file
|
||||
var dockerFile = Path.Combine(actionEntryDirectory, "Dockerfile");
|
||||
var dockerFileLowerCase = Path.Combine(actionEntryDirectory, "dockerfile");
|
||||
var actionManifest = Path.Combine(actionEntryDirectory, "action.yml");
|
||||
if (File.Exists(actionManifest))
|
||||
var actionManifest = Path.Combine(actionEntryDirectory, Constants.Path.ActionManifestYmlFile);
|
||||
var actionManifestYaml = Path.Combine(actionEntryDirectory, Constants.Path.ActionManifestYamlFile);
|
||||
if (File.Exists(actionManifest) || File.Exists(actionManifestYaml))
|
||||
{
|
||||
executionContext.Debug($"action.yml for action: '{actionManifest}'.");
|
||||
var manifestManager = HostContext.GetService<IActionManifestManager>();
|
||||
var actionDefinitionData = manifestManager.Load(executionContext, actionManifest);
|
||||
ActionDefinitionData actionDefinitionData = null;
|
||||
if (File.Exists(actionManifest))
|
||||
{
|
||||
actionDefinitionData = manifestManager.Load(executionContext, actionManifest);
|
||||
}
|
||||
else
|
||||
{
|
||||
actionDefinitionData = manifestManager.Load(executionContext, actionManifestYaml);
|
||||
}
|
||||
|
||||
if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.Container)
|
||||
{
|
||||
@@ -720,7 +752,7 @@ namespace GitHub.Runner.Worker
|
||||
else
|
||||
{
|
||||
var fullPath = IOUtil.ResolvePath(actionEntryDirectory, "."); // resolve full path without access filesystem.
|
||||
throw new InvalidOperationException($"Can't find 'action.yml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?");
|
||||
throw new InvalidOperationException($"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,17 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile);
|
||||
|
||||
List<string> EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary<string, PipelineContextData> contextData);
|
||||
List<string> EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary<string, PipelineContextData> extraExpressionValues);
|
||||
|
||||
Dictionary<string, string> EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary<string, PipelineContextData> contextData);
|
||||
Dictionary<string, string> EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary<string, PipelineContextData> extraExpressionValues);
|
||||
|
||||
string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token, IDictionary<string, PipelineContextData> contextData);
|
||||
string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token);
|
||||
}
|
||||
|
||||
public sealed class ActionManifestManager : RunnerService, IActionManifestManager
|
||||
{
|
||||
private TemplateSchema _actionManifestSchema;
|
||||
private IReadOnlyList<String> _fileTable;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
@@ -53,7 +54,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile)
|
||||
{
|
||||
var context = CreateContext(executionContext, null);
|
||||
var context = CreateContext(executionContext);
|
||||
ActionDefinitionData actionDefinition = new ActionDefinitionData();
|
||||
try
|
||||
{
|
||||
@@ -61,6 +62,9 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// Get the file ID
|
||||
var fileId = context.GetFileId(manifestFile);
|
||||
_fileTable = context.GetFileTable();
|
||||
|
||||
// Read the file
|
||||
var fileContent = File.ReadAllText(manifestFile);
|
||||
using (var stringReader = new StringReader(fileContent))
|
||||
{
|
||||
@@ -116,7 +120,7 @@ namespace GitHub.Runner.Worker
|
||||
if (actionDefinition.Execution == null)
|
||||
{
|
||||
executionContext.Debug($"Loaded action.yml file: {StringUtil.ConvertToJson(actionDefinition)}");
|
||||
throw new ArgumentException($"Top level 'run:' section is required for {manifestFile}");
|
||||
throw new ArgumentException($"Top level 'runs:' section is required for {manifestFile}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -129,13 +133,13 @@ namespace GitHub.Runner.Worker
|
||||
public List<string> EvaluateContainerArguments(
|
||||
IExecutionContext executionContext,
|
||||
SequenceToken token,
|
||||
IDictionary<string, PipelineContextData> contextData)
|
||||
IDictionary<string, PipelineContextData> extraExpressionValues)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var context = CreateContext(executionContext, contextData);
|
||||
var context = CreateContext(executionContext, extraExpressionValues);
|
||||
try
|
||||
{
|
||||
var evaluateResult = TemplateEvaluator.Evaluate(context, "container-runs-args", token, 0, null, omitHeader: true);
|
||||
@@ -168,13 +172,13 @@ namespace GitHub.Runner.Worker
|
||||
public Dictionary<string, string> EvaluateContainerEnvironment(
|
||||
IExecutionContext executionContext,
|
||||
MappingToken token,
|
||||
IDictionary<string, PipelineContextData> contextData)
|
||||
IDictionary<string, PipelineContextData> extraExpressionValues)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var context = CreateContext(executionContext, contextData);
|
||||
var context = CreateContext(executionContext, extraExpressionValues);
|
||||
try
|
||||
{
|
||||
var evaluateResult = TemplateEvaluator.Evaluate(context, "container-runs-env", token, 0, null, omitHeader: true);
|
||||
@@ -212,13 +216,12 @@ namespace GitHub.Runner.Worker
|
||||
public string EvaluateDefaultInput(
|
||||
IExecutionContext executionContext,
|
||||
string inputName,
|
||||
TemplateToken token,
|
||||
IDictionary<string, PipelineContextData> contextData)
|
||||
TemplateToken token)
|
||||
{
|
||||
string result = "";
|
||||
if (token != null)
|
||||
{
|
||||
var context = CreateContext(executionContext, contextData);
|
||||
var context = CreateContext(executionContext);
|
||||
try
|
||||
{
|
||||
var evaluateResult = TemplateEvaluator.Evaluate(context, "input-default-context", token, 0, null, omitHeader: true);
|
||||
@@ -243,7 +246,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
private TemplateContext CreateContext(
|
||||
IExecutionContext executionContext,
|
||||
IDictionary<string, PipelineContextData> contextData)
|
||||
IDictionary<string, PipelineContextData> extraExpressionValues = null)
|
||||
{
|
||||
var result = new TemplateContext
|
||||
{
|
||||
@@ -257,12 +260,34 @@ namespace GitHub.Runner.Worker
|
||||
TraceWriter = executionContext.ToTemplateTraceWriter(),
|
||||
};
|
||||
|
||||
if (contextData?.Count > 0)
|
||||
{
|
||||
foreach (var pair in contextData)
|
||||
// Expression values from execution context
|
||||
foreach (var pair in executionContext.ExpressionValues)
|
||||
{
|
||||
result.ExpressionValues[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
// Extra expression values
|
||||
if (extraExpressionValues?.Count > 0)
|
||||
{
|
||||
foreach (var pair in extraExpressionValues)
|
||||
{
|
||||
result.ExpressionValues[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Expression functions from execution context
|
||||
foreach (var item in executionContext.ExpressionFunctions)
|
||||
{
|
||||
result.ExpressionFunctions.Add(item);
|
||||
}
|
||||
|
||||
// Add the file table
|
||||
if (_fileTable?.Count > 0)
|
||||
{
|
||||
for (var i = 0 ; i < _fileTable.Count ; i++)
|
||||
{
|
||||
result.GetFileId(_fileTable[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -341,7 +366,7 @@ namespace GitHub.Runner.Worker
|
||||
EntryPoint = entrypointToken?.Value,
|
||||
Environment = envToken,
|
||||
Cleanup = postEntrypointToken?.Value,
|
||||
CleanupCondition = postIfToken?.Value
|
||||
CleanupCondition = postIfToken?.Value ?? "always()"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -357,7 +382,7 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
Script = mainToken.Value,
|
||||
Cleanup = postToken?.Value,
|
||||
CleanupCondition = postIfToken?.Value
|
||||
CleanupCondition = postIfToken?.Value ?? "always()"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -415,566 +440,5 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a YAML file into a TemplateToken
|
||||
/// </summary>
|
||||
internal sealed class YamlObjectReader : IObjectReader
|
||||
{
|
||||
internal YamlObjectReader(
|
||||
Int32? fileId,
|
||||
TextReader input)
|
||||
{
|
||||
m_fileId = fileId;
|
||||
m_parser = new Parser(input);
|
||||
}
|
||||
|
||||
public Boolean AllowLiteral(out LiteralToken value)
|
||||
{
|
||||
if (EvaluateCurrent() is Scalar scalar)
|
||||
{
|
||||
// Tag specified
|
||||
if (!string.IsNullOrEmpty(scalar.Tag))
|
||||
{
|
||||
// String tag
|
||||
if (string.Equals(scalar.Tag, c_stringTag, StringComparison.Ordinal))
|
||||
{
|
||||
value = new StringToken(m_fileId, scalar.Start.Line, scalar.Start.Column, scalar.Value);
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not plain style
|
||||
if (scalar.Style != ScalarStyle.Plain)
|
||||
{
|
||||
throw new NotSupportedException($"The scalar style '{scalar.Style}' on line {scalar.Start.Line} and column {scalar.Start.Column} is not valid with the tag '{scalar.Tag}'");
|
||||
}
|
||||
|
||||
// Boolean, Float, Integer, or Null
|
||||
switch (scalar.Tag)
|
||||
{
|
||||
case c_booleanTag:
|
||||
value = ParseBoolean(scalar);
|
||||
break;
|
||||
case c_floatTag:
|
||||
value = ParseFloat(scalar);
|
||||
break;
|
||||
case c_integerTag:
|
||||
value = ParseInteger(scalar);
|
||||
break;
|
||||
case c_nullTag:
|
||||
value = ParseNull(scalar);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected tag '{scalar.Tag}'");
|
||||
}
|
||||
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Plain style, determine type using YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
if (scalar.Style == ScalarStyle.Plain)
|
||||
{
|
||||
if (MatchNull(scalar, out var nullToken))
|
||||
{
|
||||
value = nullToken;
|
||||
}
|
||||
else if (MatchBoolean(scalar, out var booleanToken))
|
||||
{
|
||||
value = booleanToken;
|
||||
}
|
||||
else if (MatchInteger(scalar, out var numberToken) ||
|
||||
MatchFloat(scalar, out numberToken))
|
||||
{
|
||||
value = numberToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = new StringToken(m_fileId, scalar.Start.Line, scalar.Start.Column, scalar.Value);
|
||||
}
|
||||
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise assume string
|
||||
value = new StringToken(m_fileId, scalar.Start.Line, scalar.Start.Column, scalar.Value);
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Boolean AllowSequenceStart(out SequenceToken value)
|
||||
{
|
||||
if (EvaluateCurrent() is SequenceStart sequenceStart)
|
||||
{
|
||||
value = new SequenceToken(m_fileId, sequenceStart.Start.Line, sequenceStart.Start.Column);
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Boolean AllowSequenceEnd()
|
||||
{
|
||||
if (EvaluateCurrent() is SequenceEnd)
|
||||
{
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Boolean AllowMappingStart(out MappingToken value)
|
||||
{
|
||||
if (EvaluateCurrent() is MappingStart mappingStart)
|
||||
{
|
||||
value = new MappingToken(m_fileId, mappingStart.Start.Line, mappingStart.Start.Column);
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Boolean AllowMappingEnd()
|
||||
{
|
||||
if (EvaluateCurrent() is MappingEnd)
|
||||
{
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumes the last parsing events, which are expected to be DocumentEnd and StreamEnd.
|
||||
/// </summary>
|
||||
public void ValidateEnd()
|
||||
{
|
||||
if (EvaluateCurrent() is DocumentEnd)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Expected document end parse event");
|
||||
}
|
||||
|
||||
if (EvaluateCurrent() is StreamEnd)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Expected stream end parse event");
|
||||
}
|
||||
|
||||
if (MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Expected end of parse events");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumes the first parsing events, which are expected to be StreamStart and DocumentStart.
|
||||
/// </summary>
|
||||
public void ValidateStart()
|
||||
{
|
||||
if (EvaluateCurrent() != null)
|
||||
{
|
||||
throw new InvalidOperationException("Unexpected parser state");
|
||||
}
|
||||
|
||||
if (!MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Expected a parse event");
|
||||
}
|
||||
|
||||
if (EvaluateCurrent() is StreamStart)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Expected stream start parse event");
|
||||
}
|
||||
|
||||
if (EvaluateCurrent() is DocumentStart)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Expected document start parse event");
|
||||
}
|
||||
}
|
||||
|
||||
private ParsingEvent EvaluateCurrent()
|
||||
{
|
||||
if (m_current == null)
|
||||
{
|
||||
m_current = m_parser.Current;
|
||||
if (m_current != null)
|
||||
{
|
||||
if (m_current is Scalar scalar)
|
||||
{
|
||||
// Verify not using achors
|
||||
if (scalar.Anchor != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Anchors are not currently supported. Remove the anchor '{scalar.Anchor}'");
|
||||
}
|
||||
}
|
||||
else if (m_current is MappingStart mappingStart)
|
||||
{
|
||||
// Verify not using achors
|
||||
if (mappingStart.Anchor != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Anchors are not currently supported. Remove the anchor '{mappingStart.Anchor}'");
|
||||
}
|
||||
}
|
||||
else if (m_current is SequenceStart sequenceStart)
|
||||
{
|
||||
// Verify not using achors
|
||||
if (sequenceStart.Anchor != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Anchors are not currently supported. Remove the anchor '{sequenceStart.Anchor}'");
|
||||
}
|
||||
}
|
||||
else if (!(m_current is MappingEnd) &&
|
||||
!(m_current is SequenceEnd) &&
|
||||
!(m_current is DocumentStart) &&
|
||||
!(m_current is DocumentEnd) &&
|
||||
!(m_current is StreamStart) &&
|
||||
!(m_current is StreamEnd))
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected parsing event type: {m_current.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m_current;
|
||||
}
|
||||
|
||||
private Boolean MoveNext()
|
||||
{
|
||||
m_current = null;
|
||||
return m_parser.MoveNext();
|
||||
}
|
||||
|
||||
private BooleanToken ParseBoolean(Scalar scalar)
|
||||
{
|
||||
if (MatchBoolean(scalar, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
ThrowInvalidValue(scalar, c_booleanTag); // throws
|
||||
return default;
|
||||
}
|
||||
|
||||
private NumberToken ParseFloat(Scalar scalar)
|
||||
{
|
||||
if (MatchFloat(scalar, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
ThrowInvalidValue(scalar, c_floatTag); // throws
|
||||
return default;
|
||||
}
|
||||
|
||||
private NumberToken ParseInteger(Scalar scalar)
|
||||
{
|
||||
if (MatchInteger(scalar, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
return default;
|
||||
}
|
||||
|
||||
private NullToken ParseNull(Scalar scalar)
|
||||
{
|
||||
if (MatchNull(scalar, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
ThrowInvalidValue(scalar, c_nullTag); // throws
|
||||
return default;
|
||||
}
|
||||
|
||||
private Boolean MatchBoolean(
|
||||
Scalar scalar,
|
||||
out BooleanToken value)
|
||||
{
|
||||
// YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
switch (scalar.Value ?? string.Empty)
|
||||
{
|
||||
case "true":
|
||||
case "True":
|
||||
case "TRUE":
|
||||
value = new BooleanToken(m_fileId, scalar.Start.Line, scalar.Start.Column, true);
|
||||
return true;
|
||||
case "false":
|
||||
case "False":
|
||||
case "FALSE":
|
||||
value = new BooleanToken(m_fileId, scalar.Start.Line, scalar.Start.Column, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private Boolean MatchFloat(
|
||||
Scalar scalar,
|
||||
out NumberToken value)
|
||||
{
|
||||
// YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
var str = scalar.Value;
|
||||
if (!string.IsNullOrEmpty(str))
|
||||
{
|
||||
// Check for [-+]?(\.inf|\.Inf|\.INF)|\.nan|\.NaN|\.NAN
|
||||
switch (str)
|
||||
{
|
||||
case ".inf":
|
||||
case ".Inf":
|
||||
case ".INF":
|
||||
case "+.inf":
|
||||
case "+.Inf":
|
||||
case "+.INF":
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, Double.PositiveInfinity);
|
||||
return true;
|
||||
case "-.inf":
|
||||
case "-.Inf":
|
||||
case "-.INF":
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, Double.NegativeInfinity);
|
||||
return true;
|
||||
case ".nan":
|
||||
case ".NaN":
|
||||
case ".NAN":
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, Double.NaN);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise check [-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?
|
||||
|
||||
// Skip leading sign
|
||||
var index = str[0] == '-' || str[0] == '+' ? 1 : 0;
|
||||
|
||||
// Check for integer portion
|
||||
var length = str.Length;
|
||||
var hasInteger = false;
|
||||
while (index < length && str[index] >= '0' && str[index] <= '9')
|
||||
{
|
||||
hasInteger = true;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check for decimal point
|
||||
var hasDot = false;
|
||||
if (index < length && str[index] == '.')
|
||||
{
|
||||
hasDot = true;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check for decimal portion
|
||||
var hasDecimal = false;
|
||||
while (index < length && str[index] >= '0' && str[index] <= '9')
|
||||
{
|
||||
hasDecimal = true;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check [-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)
|
||||
if ((hasDot && hasDecimal) || hasInteger)
|
||||
{
|
||||
// Check for end
|
||||
if (index == length)
|
||||
{
|
||||
// Try parse
|
||||
if (Double.TryParse(str, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, doubleValue);
|
||||
return true;
|
||||
}
|
||||
// Otherwise exceeds range
|
||||
else
|
||||
{
|
||||
ThrowInvalidValue(scalar, c_floatTag); // throws
|
||||
}
|
||||
}
|
||||
// Check [eE][-+]?[0-9]
|
||||
else if (index < length && (str[index] == 'e' || str[index] == 'E'))
|
||||
{
|
||||
index++;
|
||||
|
||||
// Skip sign
|
||||
if (index < length && (str[index] == '-' || str[index] == '+'))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check for exponent
|
||||
var hasExponent = false;
|
||||
while (index < length && str[index] >= '0' && str[index] <= '9')
|
||||
{
|
||||
hasExponent = true;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check for end
|
||||
if (hasExponent && index == length)
|
||||
{
|
||||
// Try parse
|
||||
if (Double.TryParse(str, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, (Double)doubleValue);
|
||||
return true;
|
||||
}
|
||||
// Otherwise exceeds range
|
||||
else
|
||||
{
|
||||
ThrowInvalidValue(scalar, c_floatTag); // throws
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private Boolean MatchInteger(
|
||||
Scalar scalar,
|
||||
out NumberToken value)
|
||||
{
|
||||
// YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
var str = scalar.Value;
|
||||
if (!string.IsNullOrEmpty(str))
|
||||
{
|
||||
// Check for [0-9]+
|
||||
var firstChar = str[0];
|
||||
if (firstChar >= '0' && firstChar <= '9' &&
|
||||
str.Skip(1).All(x => x >= '0' && x <= '9'))
|
||||
{
|
||||
// Try parse
|
||||
if (Double.TryParse(str, NumberStyles.None, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, doubleValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise exceeds range
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
}
|
||||
// Check for (-|+)[0-9]+
|
||||
else if ((firstChar == '-' || firstChar == '+') &&
|
||||
str.Length > 1 &&
|
||||
str.Skip(1).All(x => x >= '0' && x <= '9'))
|
||||
{
|
||||
// Try parse
|
||||
if (Double.TryParse(str, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, doubleValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise exceeds range
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
}
|
||||
// Check for 0x[0-9a-fA-F]+
|
||||
else if (firstChar == '0' &&
|
||||
str.Length > 2 &&
|
||||
str[1] == 'x' &&
|
||||
str.Skip(2).All(x => (x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F')))
|
||||
{
|
||||
// Try parse
|
||||
if (Int32.TryParse(str.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var integerValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, integerValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise exceeds range
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
}
|
||||
// Check for 0o[0-9]+
|
||||
else if (firstChar == '0' &&
|
||||
str.Length > 2 &&
|
||||
str[1] == 'o' &&
|
||||
str.Skip(2).All(x => x >= '0' && x <= '7'))
|
||||
{
|
||||
// Try parse
|
||||
var integerValue = default(Int32);
|
||||
try
|
||||
{
|
||||
integerValue = Convert.ToInt32(str.Substring(2), 8);
|
||||
}
|
||||
// Otherwise exceeds range
|
||||
catch (Exception)
|
||||
{
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
}
|
||||
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, integerValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private Boolean MatchNull(
|
||||
Scalar scalar,
|
||||
out NullToken value)
|
||||
{
|
||||
// YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
switch (scalar.Value ?? string.Empty)
|
||||
{
|
||||
case "":
|
||||
case "null":
|
||||
case "Null":
|
||||
case "NULL":
|
||||
case "~":
|
||||
value = new NullToken(m_fileId, scalar.Start.Line, scalar.Start.Column);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ThrowInvalidValue(
|
||||
Scalar scalar,
|
||||
String tag)
|
||||
{
|
||||
throw new NotSupportedException($"The value '{scalar.Value}' on line {scalar.Start.Line} and column {scalar.Start.Column} is invalid for the type '{scalar.Tag}'");
|
||||
}
|
||||
|
||||
private const String c_booleanTag = "tag:yaml.org,2002:bool";
|
||||
private const String c_floatTag = "tag:yaml.org,2002:float";
|
||||
private const String c_integerTag = "tag:yaml.org,2002:int";
|
||||
private const String c_nullTag = "tag:yaml.org,2002:null";
|
||||
private const String c_stringTag = "tag:yaml.org,2002:string";
|
||||
private readonly Int32? m_fileId;
|
||||
private readonly Parser m_parser;
|
||||
private ParsingEvent m_current;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace GitHub.Runner.Worker
|
||||
public interface IActionRunner : IStep, IRunnerService
|
||||
{
|
||||
ActionRunStage Stage { get; set; }
|
||||
Boolean TryEvaluateDisplayName(DictionaryContextData contextData, IExecutionContext context);
|
||||
bool TryEvaluateDisplayName(DictionaryContextData contextData, IExecutionContext context);
|
||||
Pipelines.ActionStep Action { get; set; }
|
||||
}
|
||||
|
||||
@@ -141,10 +141,8 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// Load the inputs.
|
||||
ExecutionContext.Debug("Loading inputs");
|
||||
var templateTrace = ExecutionContext.ToTemplateTraceWriter();
|
||||
var schema = new PipelineTemplateSchemaFactory().CreateSchema();
|
||||
var templateEvaluator = new PipelineTemplateEvaluator(templateTrace, schema);
|
||||
var inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, ExecutionContext.ExpressionValues);
|
||||
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
|
||||
var inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
|
||||
|
||||
foreach (KeyValuePair<string, string> input in inputs)
|
||||
{
|
||||
@@ -164,13 +162,7 @@ namespace GitHub.Runner.Worker
|
||||
string key = input.Key.AssertString("action input name").Value;
|
||||
if (!inputs.ContainsKey(key))
|
||||
{
|
||||
var evaluateContext = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var data in ExecutionContext.ExpressionValues)
|
||||
{
|
||||
evaluateContext[data.Key] = data.Value;
|
||||
}
|
||||
|
||||
inputs[key] = manifestManager.EvaluateDefaultInput(ExecutionContext, key, input.Value, evaluateContext);
|
||||
inputs[key] = manifestManager.EvaluateDefaultInput(ExecutionContext, key, input.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,17 +171,15 @@ namespace GitHub.Runner.Worker
|
||||
ExecutionContext.Debug("Loading env");
|
||||
var environment = new Dictionary<String, String>(VarUtil.EnvironmentVariableKeyComparer);
|
||||
|
||||
// Apply environment set using ##[set-env] first since these are job level env
|
||||
foreach (var env in ExecutionContext.EnvironmentVariables)
|
||||
#if OS_WINDOWS
|
||||
var envContext = ExecutionContext.ExpressionValues["env"] as DictionaryContextData;
|
||||
#else
|
||||
var envContext = ExecutionContext.ExpressionValues["env"] as CaseSensitiveDictionaryContextData;
|
||||
#endif
|
||||
// Apply environment from env context, env context contains job level env and action's evn block
|
||||
foreach (var env in envContext)
|
||||
{
|
||||
environment[env.Key] = env.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
// Apply action's env block later.
|
||||
var actionEnvironment = templateEvaluator.EvaluateStepEnvironment(Action.Environment, ExecutionContext.ExpressionValues, VarUtil.EnvironmentVariableKeyComparer);
|
||||
foreach (var env in actionEnvironment)
|
||||
{
|
||||
environment[env.Key] = env.Value ?? string.Empty;
|
||||
environment[env.Key] = env.Value.ToString();
|
||||
}
|
||||
|
||||
// Apply action's intra-action state at last
|
||||
@@ -297,11 +287,14 @@ namespace GitHub.Runner.Worker
|
||||
return displayName;
|
||||
}
|
||||
// Try evaluating fully
|
||||
var schema = new PipelineTemplateSchemaFactory().CreateSchema();
|
||||
var templateEvaluator = new PipelineTemplateEvaluator(context.ToTemplateTraceWriter(), schema);
|
||||
try
|
||||
{
|
||||
didFullyEvaluate = templateEvaluator.TryEvaluateStepDisplayName(tokenToParse, contextData, out displayName);
|
||||
if (tokenToParse.CheckHasRequiredContext(contextData, context.ExpressionFunctions))
|
||||
{
|
||||
var templateEvaluator = context.ToPipelineTemplateEvaluator();
|
||||
displayName = templateEvaluator.EvaluateStepDisplayName(tokenToParse, contextData, context.ExpressionFunctions);
|
||||
didFullyEvaluate = true;
|
||||
}
|
||||
}
|
||||
catch (TemplateValidationException e)
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker.Container
|
||||
{
|
||||
@@ -19,7 +19,6 @@ namespace GitHub.Runner.Worker.Container
|
||||
|
||||
public ContainerInfo()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ContainerInfo(IHostContext hostContext, Pipelines.JobContainer container, bool isJobContainer = true, string networkAlias = null)
|
||||
@@ -62,8 +61,11 @@ namespace GitHub.Runner.Worker.Container
|
||||
foreach (var volume in container.Volumes)
|
||||
{
|
||||
UserMountVolumes[volume] = volume;
|
||||
MountVolumes.Add(new MountVolume(volume));
|
||||
}
|
||||
}
|
||||
|
||||
UpdateWebProxyEnv(hostContext.WebProxy);
|
||||
}
|
||||
|
||||
public string ContainerId { get; set; }
|
||||
@@ -223,6 +225,26 @@ namespace GitHub.Runner.Worker.Container
|
||||
{
|
||||
_pathMappings.Insert(0, new PathMapping(hostCommonPath, containerCommonPath));
|
||||
}
|
||||
|
||||
private void UpdateWebProxyEnv(RunnerWebProxy webProxy)
|
||||
{
|
||||
// Set common forms of proxy variables if configured in Runner and not set directly by container.env
|
||||
if (!String.IsNullOrEmpty(webProxy.HttpProxyAddress))
|
||||
{
|
||||
ContainerEnvironmentVariables.TryAdd("HTTP_PROXY", webProxy.HttpProxyAddress);
|
||||
ContainerEnvironmentVariables.TryAdd("http_proxy", webProxy.HttpProxyAddress);
|
||||
}
|
||||
if (!String.IsNullOrEmpty(webProxy.HttpsProxyAddress))
|
||||
{
|
||||
ContainerEnvironmentVariables.TryAdd("HTTPS_PROXY", webProxy.HttpsProxyAddress);
|
||||
ContainerEnvironmentVariables.TryAdd("https_proxy", webProxy.HttpsProxyAddress);
|
||||
}
|
||||
if (!String.IsNullOrEmpty(webProxy.NoProxyString))
|
||||
{
|
||||
ContainerEnvironmentVariables.TryAdd("NO_PROXY", webProxy.NoProxyString);
|
||||
ContainerEnvironmentVariables.TryAdd("no_proxy", webProxy.NoProxyString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MountVolume
|
||||
|
||||
@@ -130,6 +130,13 @@ namespace GitHub.Runner.Worker.Container
|
||||
// Watermark for GitHub Action environment
|
||||
dockerOptions.Add("-e GITHUB_ACTIONS=true");
|
||||
|
||||
// Set CI=true when no one else already set it.
|
||||
// CI=true is common set in most CI provider in GitHub
|
||||
if (!container.ContainerEnvironmentVariables.ContainsKey("CI"))
|
||||
{
|
||||
dockerOptions.Add("-e CI=true");
|
||||
}
|
||||
|
||||
foreach (var volume in container.MountVolumes)
|
||||
{
|
||||
// replace `"` with `\"` and add `"{0}"` to all path.
|
||||
@@ -189,6 +196,13 @@ namespace GitHub.Runner.Worker.Container
|
||||
// Watermark for GitHub Action environment
|
||||
dockerOptions.Add("-e GITHUB_ACTIONS=true");
|
||||
|
||||
// Set CI=true when no one else already set it.
|
||||
// CI=true is common set in most CI provider in GitHub
|
||||
if (!container.ContainerEnvironmentVariables.ContainsKey("CI"))
|
||||
{
|
||||
dockerOptions.Add("-e CI=true");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(container.ContainerEntryPoint))
|
||||
{
|
||||
dockerOptions.Add($"--entrypoint \"{container.ContainerEntryPoint}\"");
|
||||
|
||||
@@ -180,6 +180,11 @@ namespace GitHub.Runner.Worker
|
||||
foreach (var volume in container.UserMountVolumes)
|
||||
{
|
||||
Trace.Info($"User provided volume: {volume.Value}");
|
||||
var mount = new MountVolume(volume.Value);
|
||||
if (string.Equals(mount.SourceVolumePath, "/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
executionContext.Warning($"Volume mount {volume.Value} is going to mount '/' into the container which may cause file ownership change in the entire file system and cause Actions Runner to lose permission to access the disk.");
|
||||
}
|
||||
}
|
||||
|
||||
// Pull down docker image with retry up to 3 times
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Services.WebApi;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using System.Collections;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
@@ -37,6 +39,7 @@ namespace GitHub.Runner.Worker
|
||||
string ContextName { get; }
|
||||
Task ForceCompleted { get; }
|
||||
TaskResult? Result { get; set; }
|
||||
TaskResult? Outcome { get; set; }
|
||||
string ResultCode { get; set; }
|
||||
TaskResult? CommandResult { get; set; }
|
||||
CancellationToken CancellationToken { get; }
|
||||
@@ -45,11 +48,14 @@ namespace GitHub.Runner.Worker
|
||||
PlanFeatures Features { get; }
|
||||
Variables Variables { get; }
|
||||
Dictionary<string, string> IntraActionState { get; }
|
||||
HashSet<string> OutputVariables { get; }
|
||||
IDictionary<String, IDictionary<String, String>> JobDefaults { get; }
|
||||
Dictionary<string, VariableValue> JobOutputs { get; }
|
||||
IDictionary<String, String> EnvironmentVariables { get; }
|
||||
IDictionary<String, ContextScope> Scopes { get; }
|
||||
IList<String> FileTable { get; }
|
||||
StepsContext StepsContext { get; }
|
||||
DictionaryContextData ExpressionValues { get; }
|
||||
IList<IFunctionInfo> ExpressionFunctions { get; }
|
||||
List<string> PrependPath { get; }
|
||||
ContainerInfo Container { get; set; }
|
||||
List<ContainerInfo> ServiceContainers { get; }
|
||||
@@ -107,7 +113,6 @@ namespace GitHub.Runner.Worker
|
||||
private readonly TimelineRecord _record = new TimelineRecord();
|
||||
private readonly Dictionary<Guid, TimelineRecord> _detailRecords = new Dictionary<Guid, TimelineRecord>();
|
||||
private readonly object _loggerLock = new object();
|
||||
private readonly HashSet<string> _outputvariables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly object _matchersLock = new object();
|
||||
|
||||
private event OnMatcherChanged _onMatcherChanged;
|
||||
@@ -137,11 +142,14 @@ namespace GitHub.Runner.Worker
|
||||
public List<ServiceEndpoint> Endpoints { get; private set; }
|
||||
public Variables Variables { get; private set; }
|
||||
public Dictionary<string, string> IntraActionState { get; private set; }
|
||||
public HashSet<string> OutputVariables => _outputvariables;
|
||||
public IDictionary<String, IDictionary<String, String>> JobDefaults { get; private set; }
|
||||
public Dictionary<string, VariableValue> JobOutputs { get; private set; }
|
||||
public IDictionary<String, String> EnvironmentVariables { get; private set; }
|
||||
public IDictionary<String, ContextScope> Scopes { get; private set; }
|
||||
public IList<String> FileTable { get; private set; }
|
||||
public StepsContext StepsContext { get; private set; }
|
||||
public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData();
|
||||
public IList<IFunctionInfo> ExpressionFunctions { get; } = new List<IFunctionInfo>();
|
||||
public bool WriteDebug { get; private set; }
|
||||
public List<string> PrependPath { get; private set; }
|
||||
public ContainerInfo Container { get; set; }
|
||||
@@ -168,6 +176,8 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
public TaskResult? Outcome { get; set; }
|
||||
|
||||
public TaskResult? CommandResult { get; set; }
|
||||
|
||||
private string ContextType => _record.RecordType;
|
||||
@@ -264,12 +274,18 @@ namespace GitHub.Runner.Worker
|
||||
child.IntraActionState = intraActionState;
|
||||
}
|
||||
child.EnvironmentVariables = EnvironmentVariables;
|
||||
child.JobDefaults = JobDefaults;
|
||||
child.Scopes = Scopes;
|
||||
child.FileTable = FileTable;
|
||||
child.StepsContext = StepsContext;
|
||||
foreach (var pair in ExpressionValues)
|
||||
{
|
||||
child.ExpressionValues[pair.Key] = pair.Value;
|
||||
}
|
||||
foreach (var item in ExpressionFunctions)
|
||||
{
|
||||
child.ExpressionFunctions.Add(item);
|
||||
}
|
||||
child._cancellationTokenSource = new CancellationTokenSource();
|
||||
child.WriteDebug = WriteDebug;
|
||||
child._parentExecutionContext = this;
|
||||
@@ -342,6 +358,12 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
_logger.End();
|
||||
|
||||
if (!string.IsNullOrEmpty(ContextName))
|
||||
{
|
||||
StepsContext.SetOutcome(ScopeName, ContextName, (Outcome ?? Result ?? TaskResult.Succeeded).ToActionResult().ToString());
|
||||
StepsContext.SetConclusion(ScopeName, ContextName, (Result ?? TaskResult.Succeeded).ToActionResult().ToString());
|
||||
}
|
||||
|
||||
return Result.Value;
|
||||
}
|
||||
|
||||
@@ -552,6 +574,12 @@ namespace GitHub.Runner.Worker
|
||||
// Environment variables shared across all actions
|
||||
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer);
|
||||
|
||||
// Job defaults shared across all actions
|
||||
JobDefaults = new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Job Outputs
|
||||
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Service container info
|
||||
ServiceContainers = new List<ContainerInfo>();
|
||||
|
||||
@@ -568,6 +596,9 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
// File table
|
||||
FileTable = new List<String>(message.FileTable ?? new string[0]);
|
||||
|
||||
// Expression values
|
||||
if (message.ContextData?.Count > 0)
|
||||
{
|
||||
@@ -585,8 +616,13 @@ namespace GitHub.Runner.Worker
|
||||
var githubAccessToken = new StringContextData(Variables.Get("system.github.token"));
|
||||
var base64EncodedToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{githubAccessToken}"));
|
||||
HostContext.SecretMasker.AddValue(base64EncodedToken);
|
||||
var githubJob = Variables.Get("system.github.job");
|
||||
var githubContext = new GitHubContext();
|
||||
githubContext["token"] = githubAccessToken;
|
||||
if (!string.IsNullOrEmpty(githubJob))
|
||||
{
|
||||
githubContext["job"] = new StringContextData(githubJob);
|
||||
}
|
||||
var githubDictionary = ExpressionValues["github"].AssertDictionary("github");
|
||||
foreach (var pair in githubDictionary)
|
||||
{
|
||||
@@ -722,7 +758,7 @@ namespace GitHub.Runner.Worker
|
||||
var owners = config.Matchers.Select(x => $"'{x.Owner}'");
|
||||
var joinedOwners = string.Join(", ", owners);
|
||||
// todo: loc
|
||||
this.Output($"Added matchers: {joinedOwners}. Problem matchers scan action output for known warning or error strings and report these inline.");
|
||||
this.Debug($"Added matchers: {joinedOwners}. Problem matchers scan action output for known warning or error strings and report these inline.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,7 +800,7 @@ namespace GitHub.Runner.Worker
|
||||
owners = removedMatchers.Select(x => $"'{x.Owner}'");
|
||||
var joinedOwners = string.Join(", ", owners);
|
||||
// todo: loc
|
||||
this.Output($"Removed matchers: {joinedOwners}");
|
||||
this.Debug($"Removed matchers: {joinedOwners}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,6 +915,21 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<KeyValuePair<string, object>> ToExpressionState(this IExecutionContext context)
|
||||
{
|
||||
return new[] { new KeyValuePair<string, object>(nameof(IExecutionContext), context) };
|
||||
}
|
||||
|
||||
public static PipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null)
|
||||
{
|
||||
if (traceWriter == null)
|
||||
{
|
||||
traceWriter = context.ToTemplateTraceWriter();
|
||||
}
|
||||
var schema = PipelineTemplateSchemaFactory.GetSchema();
|
||||
return new PipelineTemplateEvaluator(traceWriter, schema, context.FileTable);
|
||||
}
|
||||
|
||||
public static ObjectTemplating.ITraceWriter ToTemplateTraceWriter(this IExecutionContext context)
|
||||
{
|
||||
return new TemplateTraceWriter(context);
|
||||
@@ -891,6 +942,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
internal TemplateTraceWriter(IExecutionContext executionContext)
|
||||
{
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
_executionContext = executionContext;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
[ServiceLocator(Default = typeof(ExpressionManager))]
|
||||
public interface IExpressionManager : IRunnerService
|
||||
{
|
||||
ConditionResult Evaluate(IExecutionContext context, string condition, bool hostTracingOnly = false);
|
||||
}
|
||||
|
||||
public sealed class ExpressionManager : RunnerService, IExpressionManager
|
||||
{
|
||||
public ConditionResult Evaluate(IExecutionContext executionContext, string condition, bool hostTracingOnly = false)
|
||||
{
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
|
||||
ConditionResult result = new ConditionResult();
|
||||
var expressionTrace = new TraceWriter(Trace, hostTracingOnly ? null : executionContext);
|
||||
var tree = Parse(executionContext, expressionTrace, condition);
|
||||
var expressionResult = tree.Evaluate(expressionTrace, HostContext.SecretMasker, state: executionContext, options: null);
|
||||
result.Value = expressionResult.IsTruthy;
|
||||
result.Trace = expressionTrace.Trace;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IExpressionNode Parse(IExecutionContext executionContext, TraceWriter expressionTrace, string condition)
|
||||
{
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(condition))
|
||||
{
|
||||
condition = $"{PipelineTemplateConstants.Success}()";
|
||||
}
|
||||
|
||||
var parser = new ExpressionParser();
|
||||
var namedValues = executionContext.ExpressionValues.Keys.Select(x => new NamedValueInfo<ContextValueNode>(x)).ToArray();
|
||||
var functions = new IFunctionInfo[]
|
||||
{
|
||||
new FunctionInfo<AlwaysNode>(name: Constants.Expressions.Always, minParameters: 0, maxParameters: 0),
|
||||
new FunctionInfo<CancelledNode>(name: Constants.Expressions.Cancelled, minParameters: 0, maxParameters: 0),
|
||||
new FunctionInfo<FailureNode>(name: Constants.Expressions.Failure, minParameters: 0, maxParameters: 0),
|
||||
new FunctionInfo<SuccessNode>(name: Constants.Expressions.Success, minParameters: 0, maxParameters: 0),
|
||||
};
|
||||
return parser.CreateTree(condition, expressionTrace, namedValues, functions) ?? new SuccessNode();
|
||||
}
|
||||
|
||||
private sealed class TraceWriter : DistributedTask.Expressions2.ITraceWriter
|
||||
{
|
||||
private readonly IExecutionContext _executionContext;
|
||||
private readonly Tracing _trace;
|
||||
private readonly StringBuilder _traceBuilder = new StringBuilder();
|
||||
|
||||
public string Trace => _traceBuilder.ToString();
|
||||
|
||||
public TraceWriter(Tracing trace, IExecutionContext executionContext)
|
||||
{
|
||||
ArgUtil.NotNull(trace, nameof(trace));
|
||||
_trace = trace;
|
||||
_executionContext = executionContext;
|
||||
}
|
||||
|
||||
public void Info(string message)
|
||||
{
|
||||
_trace.Info(message);
|
||||
_executionContext?.Debug(message);
|
||||
_traceBuilder.AppendLine(message);
|
||||
}
|
||||
|
||||
public void Verbose(string message)
|
||||
{
|
||||
_trace.Verbose(message);
|
||||
_executionContext?.Debug(message);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AlwaysNode : Function
|
||||
{
|
||||
protected override Object EvaluateCore(EvaluationContext context, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CancelledNode : Function
|
||||
{
|
||||
protected sealed override object EvaluateCore(EvaluationContext evaluationContext, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var executionContext = evaluationContext.State as IExecutionContext;
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
||||
return jobStatus == ActionResult.Cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FailureNode : Function
|
||||
{
|
||||
protected sealed override object EvaluateCore(EvaluationContext evaluationContext, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var executionContext = evaluationContext.State as IExecutionContext;
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
||||
return jobStatus == ActionResult.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SuccessNode : Function
|
||||
{
|
||||
protected sealed override object EvaluateCore(EvaluationContext evaluationContext, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var executionContext = evaluationContext.State as IExecutionContext;
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
||||
return jobStatus == ActionResult.Success;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ContextValueNode : NamedValue
|
||||
{
|
||||
protected override Object EvaluateCore(EvaluationContext evaluationContext, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var jobContext = evaluationContext.State as IExecutionContext;
|
||||
ArgUtil.NotNull(jobContext, nameof(jobContext));
|
||||
return jobContext.ExpressionValues[Name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ConditionResult
|
||||
{
|
||||
public ConditionResult(bool value = false, string trace = null)
|
||||
{
|
||||
this.Value = value;
|
||||
this.Trace = trace;
|
||||
}
|
||||
|
||||
public bool Value { get; set; }
|
||||
public string Trace { get; set; }
|
||||
|
||||
public static implicit operator ConditionResult(bool value)
|
||||
{
|
||||
return new ConditionResult(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Runner.Worker/Expressions/AlwaysFunction.cs
Normal file
25
src/Runner.Worker/Expressions/AlwaysFunction.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants;
|
||||
|
||||
namespace GitHub.Runner.Worker.Expressions
|
||||
{
|
||||
public sealed class AlwaysFunction : Function
|
||||
{
|
||||
protected override Object EvaluateCore(EvaluationContext context, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/Runner.Worker/Expressions/CancelledFunction.cs
Normal file
31
src/Runner.Worker/Expressions/CancelledFunction.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.ObjectTemplating;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants;
|
||||
|
||||
namespace GitHub.Runner.Worker.Expressions
|
||||
{
|
||||
public sealed class CancelledFunction : Function
|
||||
{
|
||||
protected sealed override object EvaluateCore(EvaluationContext evaluationContext, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var templateContext = evaluationContext.State as TemplateContext;
|
||||
ArgUtil.NotNull(templateContext, nameof(templateContext));
|
||||
var executionContext = templateContext.State[nameof(IExecutionContext)] as IExecutionContext;
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
||||
return jobStatus == ActionResult.Cancelled;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/Runner.Worker/Expressions/FailureFunction.cs
Normal file
31
src/Runner.Worker/Expressions/FailureFunction.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.ObjectTemplating;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants;
|
||||
|
||||
namespace GitHub.Runner.Worker.Expressions
|
||||
{
|
||||
public sealed class FailureFunction : Function
|
||||
{
|
||||
protected sealed override object EvaluateCore(EvaluationContext evaluationContext, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var templateContext = evaluationContext.State as TemplateContext;
|
||||
ArgUtil.NotNull(templateContext, nameof(templateContext));
|
||||
var executionContext = templateContext.State[nameof(IExecutionContext)] as IExecutionContext;
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
||||
return jobStatus == ActionResult.Failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/Runner.Worker/Expressions/HashFilesFunction.cs
Normal file
126
src/Runner.Worker/Expressions/HashFilesFunction.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.Runner.Worker.Expressions
|
||||
{
|
||||
public sealed class HashFilesFunction : Function
|
||||
{
|
||||
protected sealed override Object EvaluateCore(
|
||||
EvaluationContext context,
|
||||
out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var templateContext = context.State as DistributedTask.ObjectTemplating.TemplateContext;
|
||||
ArgUtil.NotNull(templateContext, nameof(templateContext));
|
||||
templateContext.ExpressionValues.TryGetValue(PipelineTemplateConstants.GitHub, out var githubContextData);
|
||||
ArgUtil.NotNull(githubContextData, nameof(githubContextData));
|
||||
var githubContext = githubContextData as DictionaryContextData;
|
||||
ArgUtil.NotNull(githubContext, nameof(githubContext));
|
||||
githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out var workspace);
|
||||
var workspaceData = workspace as StringContextData;
|
||||
ArgUtil.NotNull(workspaceData, nameof(workspaceData));
|
||||
|
||||
string githubWorkspace = workspaceData.Value;
|
||||
bool followSymlink = false;
|
||||
List<string> patterns = new List<string>();
|
||||
var firstParameter = true;
|
||||
foreach (var parameter in Parameters)
|
||||
{
|
||||
var parameterString = parameter.Evaluate(context).ConvertToString();
|
||||
if (firstParameter)
|
||||
{
|
||||
firstParameter = false;
|
||||
if (parameterString.StartsWith("--"))
|
||||
{
|
||||
if (string.Equals(parameterString, "--follow-symbolic-links", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
followSymlink = true;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Invalid glob option {parameterString}, avaliable option: '--follow-symbolic-links'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patterns.Add(parameterString);
|
||||
}
|
||||
|
||||
context.Trace.Info($"Search root directory: '{githubWorkspace}'");
|
||||
context.Trace.Info($"Search pattern: '{string.Join(", ", patterns)}'");
|
||||
|
||||
string binDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||
string runnerRoot = new DirectoryInfo(binDir).Parent.FullName;
|
||||
|
||||
string node = Path.Combine(runnerRoot, "externals", "node12", "bin", $"node{IOUtil.ExeExtension}");
|
||||
string hashFilesScript = Path.Combine(binDir, "hashFiles");
|
||||
var hashResult = string.Empty;
|
||||
var p = new ProcessInvoker(new HashFilesTrace(context.Trace));
|
||||
p.ErrorDataReceived += ((_, data) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(data.Data) && data.Data.StartsWith("__OUTPUT__") && data.Data.EndsWith("__OUTPUT__"))
|
||||
{
|
||||
hashResult = data.Data.Substring(10, data.Data.Length - 20);
|
||||
context.Trace.Info($"Hash result: '{hashResult}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Trace.Info(data.Data);
|
||||
}
|
||||
});
|
||||
|
||||
p.OutputDataReceived += ((_, data) =>
|
||||
{
|
||||
context.Trace.Info(data.Data);
|
||||
});
|
||||
|
||||
var env = new Dictionary<string, string>();
|
||||
if (followSymlink)
|
||||
{
|
||||
env["followSymbolicLinks"] = "true";
|
||||
}
|
||||
env["patterns"] = string.Join(Environment.NewLine, patterns);
|
||||
|
||||
int exitCode = p.ExecuteAsync(workingDirectory: githubWorkspace,
|
||||
fileName: node,
|
||||
arguments: $"\"{hashFilesScript.Replace("\"", "\\\"")}\"",
|
||||
environment: env,
|
||||
requireExitCodeZero: false,
|
||||
cancellationToken: new CancellationTokenSource(TimeSpan.FromSeconds(120)).Token).GetAwaiter().GetResult();
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"hashFiles('{ExpressionUtility.StringEscape(string.Join(", ", patterns))}') failed. Fail to hash files under directory '{githubWorkspace}'");
|
||||
}
|
||||
|
||||
return hashResult;
|
||||
}
|
||||
|
||||
private sealed class HashFilesTrace : ITraceWriter
|
||||
{
|
||||
private GitHub.DistributedTask.Expressions2.ITraceWriter _trace;
|
||||
|
||||
public HashFilesTrace(GitHub.DistributedTask.Expressions2.ITraceWriter trace)
|
||||
{
|
||||
_trace = trace;
|
||||
}
|
||||
public void Info(string message)
|
||||
{
|
||||
_trace.Info(message);
|
||||
}
|
||||
|
||||
public void Verbose(string message)
|
||||
{
|
||||
_trace.Info(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/Runner.Worker/Expressions/SuccessFunction.cs
Normal file
31
src/Runner.Worker/Expressions/SuccessFunction.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.ObjectTemplating;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants;
|
||||
|
||||
namespace GitHub.Runner.Worker.Expressions
|
||||
{
|
||||
public sealed class SuccessFunction : Function
|
||||
{
|
||||
protected sealed override object EvaluateCore(EvaluationContext evaluationContext, out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var templateContext = evaluationContext.State as TemplateContext;
|
||||
ArgUtil.NotNull(templateContext, nameof(templateContext));
|
||||
var executionContext = templateContext.State[nameof(IExecutionContext)] as IExecutionContext;
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
||||
return jobStatus == ActionResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,19 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
"action",
|
||||
"actor",
|
||||
"api_url", // temp for GHES alpha release
|
||||
"base_ref",
|
||||
"event_name",
|
||||
"event_path",
|
||||
"head_ref",
|
||||
"job",
|
||||
"ref",
|
||||
"repository",
|
||||
"repository_owner",
|
||||
"run_id",
|
||||
"run_number",
|
||||
"sha",
|
||||
"url", // temp for GHES alpha release
|
||||
"workflow",
|
||||
"workspace",
|
||||
};
|
||||
|
||||
@@ -97,14 +97,14 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
}
|
||||
}
|
||||
|
||||
var evaluateContext = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||
evaluateContext["inputs"] = inputsContext;
|
||||
var extraExpressionValues = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||
extraExpressionValues["inputs"] = inputsContext;
|
||||
|
||||
var manifestManager = HostContext.GetService<IActionManifestManager>();
|
||||
if (Data.Arguments != null)
|
||||
{
|
||||
container.ContainerEntryPointArgs = "";
|
||||
var evaluatedArgs = manifestManager.EvaluateContainerArguments(ExecutionContext, Data.Arguments, evaluateContext);
|
||||
var evaluatedArgs = manifestManager.EvaluateContainerArguments(ExecutionContext, Data.Arguments, extraExpressionValues);
|
||||
foreach (var arg in evaluatedArgs)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(arg))
|
||||
@@ -124,7 +124,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
if (Data.Environment != null)
|
||||
{
|
||||
var evaluatedEnv = manifestManager.EvaluateContainerEnvironment(ExecutionContext, Data.Environment, evaluateContext);
|
||||
var evaluatedEnv = manifestManager.EvaluateContainerEnvironment(ExecutionContext, Data.Environment, extraExpressionValues);
|
||||
foreach (var env in evaluatedEnv)
|
||||
{
|
||||
if (!this.Environment.ContainsKey(env.Key))
|
||||
@@ -189,13 +189,13 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value);
|
||||
}
|
||||
|
||||
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
||||
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
||||
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager, container))
|
||||
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager, container))
|
||||
{
|
||||
var runExitCode = await dockerManger.DockerRun(ExecutionContext, container, stdoutManager.OnDataReceived, stderrManager.OnDataReceived);
|
||||
ExecutionContext.Debug($"Docker Action run completed with exit code {runExitCode}");
|
||||
if (runExitCode != 0)
|
||||
{
|
||||
ExecutionContext.Error($"Docker run failed with exit code {runExitCode}");
|
||||
ExecutionContext.Result = TaskResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,9 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
else
|
||||
{
|
||||
var exitCode = await step;
|
||||
ExecutionContext.Debug($"Node Action run completed with exit code {exitCode}");
|
||||
if (exitCode != 0)
|
||||
{
|
||||
ExecutionContext.Error($"Node run failed with exit code {exitCode}");
|
||||
ExecutionContext.Result = TaskResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using DTWebApi = GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Worker.Handlers
|
||||
@@ -17,6 +18,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
private const string _timeoutKey = "GITHUB_ACTIONS_RUNNER_ISSUE_MATCHER_TIMEOUT";
|
||||
private static readonly Regex _colorCodeRegex = new Regex(@"\x0033\[[0-9;]*m?", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
private readonly IActionCommandManager _commandManager;
|
||||
private readonly ContainerInfo _container;
|
||||
private readonly IExecutionContext _executionContext;
|
||||
private readonly int _failsafe = 50;
|
||||
private readonly object _matchersLock = new object();
|
||||
@@ -25,10 +27,11 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
// Mapping that indicates whether a directory belongs to the workflow repository
|
||||
private readonly Dictionary<string, string> _directoryMap = new Dictionary<string, string>();
|
||||
|
||||
public OutputManager(IExecutionContext executionContext, IActionCommandManager commandManager)
|
||||
public OutputManager(IExecutionContext executionContext, IActionCommandManager commandManager, ContainerInfo container = null)
|
||||
{
|
||||
_executionContext = executionContext;
|
||||
_commandManager = commandManager;
|
||||
_container = container ?? executionContext.Container;
|
||||
|
||||
// Recursion failsafe (test override)
|
||||
var failsafeString = Environment.GetEnvironmentVariable("RUNNER_TEST_GET_REPOSITORY_PATH_FAILSAFE");
|
||||
@@ -81,7 +84,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
{
|
||||
// This does not need to be inside of a critical section.
|
||||
// The logging queues and command handlers are thread-safe.
|
||||
if (_commandManager.TryProcessCommand(_executionContext, line))
|
||||
if (_commandManager.TryProcessCommand(_executionContext, line, _container))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -257,6 +260,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
if (!string.IsNullOrWhiteSpace(match.File))
|
||||
{
|
||||
var file = match.File;
|
||||
var translate = _container != null;
|
||||
|
||||
// Root using fromPath
|
||||
if (!string.IsNullOrWhiteSpace(match.FromPath) && !Path.IsPathFullyQualified(file))
|
||||
@@ -275,11 +279,19 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
ArgUtil.NotNullOrEmpty(workspace, "workspace");
|
||||
|
||||
file = Path.Combine(workspace, file);
|
||||
translate = false;
|
||||
}
|
||||
|
||||
// Remove relative pathing and normalize slashes
|
||||
file = Path.GetFullPath(file);
|
||||
|
||||
// Translate to host
|
||||
if (translate)
|
||||
{
|
||||
file = _container.TranslateToHostPath(file);
|
||||
file = Path.GetFullPath(file);
|
||||
}
|
||||
|
||||
// Check whether the file exists
|
||||
if (File.Exists(file))
|
||||
{
|
||||
|
||||
@@ -58,7 +58,16 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
string shellCommandPath = null;
|
||||
bool validateShellOnHost = !(StepHost is ContainerStepHost);
|
||||
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse<string>());
|
||||
Inputs.TryGetValue("shell", out var shell);
|
||||
string shell = null;
|
||||
if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell))
|
||||
{
|
||||
// TODO: figure out how defaults interact with template later
|
||||
// for now, we won't check job.defaults if we are inside a template.
|
||||
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults))
|
||||
{
|
||||
runDefaults.TryGetValue("shell", out shell);
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrEmpty(shell))
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
@@ -139,11 +148,36 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
Inputs.TryGetValue("script", out var contents);
|
||||
contents = contents ?? string.Empty;
|
||||
|
||||
Inputs.TryGetValue("workingDirectory", out var workingDirectory);
|
||||
string workingDirectory = null;
|
||||
if (!Inputs.TryGetValue("workingDirectory", out workingDirectory))
|
||||
{
|
||||
// TODO: figure out how defaults interact with template later
|
||||
// for now, we won't check job.defaults if we are inside a template.
|
||||
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults))
|
||||
{
|
||||
if (runDefaults.TryGetValue("working-directory", out workingDirectory))
|
||||
{
|
||||
ExecutionContext.Debug("Overwrite 'working-directory' base on job defaults.");
|
||||
}
|
||||
}
|
||||
}
|
||||
var workspaceDir = githubContext["workspace"] as StringContextData;
|
||||
workingDirectory = Path.Combine(workspaceDir, workingDirectory ?? string.Empty);
|
||||
|
||||
Inputs.TryGetValue("shell", out var shell);
|
||||
string shell = null;
|
||||
if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell))
|
||||
{
|
||||
// TODO: figure out how defaults interact with template later
|
||||
// for now, we won't check job.defaults if we are inside a template.
|
||||
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults))
|
||||
{
|
||||
if (runDefaults.TryGetValue("shell", out shell))
|
||||
{
|
||||
ExecutionContext.Debug("Overwrite 'shell' base on job defaults.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isContainerStepHost = StepHost is ContainerStepHost;
|
||||
|
||||
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse<string>());
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
@@ -14,6 +18,16 @@ using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
[DataContract]
|
||||
public class SetupInfo
|
||||
{
|
||||
[DataMember]
|
||||
public string Group { get; set; }
|
||||
|
||||
[DataMember]
|
||||
public string Detail { get; set; }
|
||||
}
|
||||
|
||||
[ServiceLocator(Default = typeof(JobExtension))]
|
||||
|
||||
public interface IJobExtension : IRunnerService
|
||||
@@ -49,6 +63,44 @@ namespace GitHub.Runner.Worker
|
||||
context.Start();
|
||||
context.Debug($"Starting: Set up job");
|
||||
context.Output($"Current runner version: '{BuildConstants.RunnerPackage.Version}'");
|
||||
|
||||
var setupInfoFile = HostContext.GetConfigFile(WellKnownConfigFile.SetupInfo);
|
||||
if (File.Exists(setupInfoFile))
|
||||
{
|
||||
Trace.Info($"Load machine setup info from {setupInfoFile}");
|
||||
try
|
||||
{
|
||||
var setupInfo = IOUtil.LoadObject<List<SetupInfo>>(setupInfoFile);
|
||||
if (setupInfo?.Count > 0)
|
||||
{
|
||||
foreach (var info in setupInfo)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(info?.Detail))
|
||||
{
|
||||
var groupName = info.Group;
|
||||
if (string.IsNullOrEmpty(groupName))
|
||||
{
|
||||
groupName = "Machine Setup Info";
|
||||
}
|
||||
|
||||
context.Output($"##[group]{groupName}");
|
||||
var multiLines = info.Detail.Replace("\r\n", "\n").TrimEnd('\n').Split('\n');
|
||||
foreach (var line in multiLines)
|
||||
{
|
||||
context.Output(line);
|
||||
}
|
||||
context.Output("##[endgroup]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Output($"Fail to load and print machine setup info: {ex.Message}");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
var repoFullName = context.GetGitHubContext("repository");
|
||||
ArgUtil.NotNull(repoFullName, nameof(repoFullName));
|
||||
context.Debug($"Primary repository: {repoFullName}");
|
||||
@@ -76,14 +128,23 @@ namespace GitHub.Runner.Worker
|
||||
context.SetRunnerContext("workspace", Path.Combine(_workDirectory, trackingConfig.PipelineDirectory));
|
||||
context.SetGitHubContext("workspace", Path.Combine(_workDirectory, trackingConfig.WorkspaceDirectory));
|
||||
|
||||
// Temporary hack for GHES alpha
|
||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
||||
var runnerSettings = configurationStore.GetSettings();
|
||||
if (!runnerSettings.IsHostedServer && !string.IsNullOrEmpty(runnerSettings.GitHubUrl))
|
||||
{
|
||||
var url = new Uri(runnerSettings.GitHubUrl);
|
||||
var portInfo = url.IsDefaultPort ? string.Empty : $":{url.Port.ToString(CultureInfo.InvariantCulture)}";
|
||||
context.SetGitHubContext("url", $"{url.Scheme}://{url.Host}{portInfo}");
|
||||
context.SetGitHubContext("api_url", $"{url.Scheme}://{url.Host}{portInfo}/api/v3");
|
||||
}
|
||||
|
||||
// Evaluate the job-level environment variables
|
||||
context.Debug("Evaluating job-level environment variables");
|
||||
var templateTrace = context.ToTemplateTraceWriter();
|
||||
var schema = new PipelineTemplateSchemaFactory().CreateSchema();
|
||||
var templateEvaluator = new PipelineTemplateEvaluator(templateTrace, schema);
|
||||
var templateEvaluator = context.ToPipelineTemplateEvaluator();
|
||||
foreach (var token in message.EnvironmentVariables)
|
||||
{
|
||||
var environmentVariables = templateEvaluator.EvaluateStepEnvironment(token, jobContext.ExpressionValues, VarUtil.EnvironmentVariableKeyComparer);
|
||||
var environmentVariables = templateEvaluator.EvaluateStepEnvironment(token, jobContext.ExpressionValues, jobContext.ExpressionFunctions, VarUtil.EnvironmentVariableKeyComparer);
|
||||
foreach (var pair in environmentVariables)
|
||||
{
|
||||
context.EnvironmentVariables[pair.Key] = pair.Value ?? string.Empty;
|
||||
@@ -93,7 +154,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// Evaluate the job container
|
||||
context.Debug("Evaluating job container");
|
||||
var container = templateEvaluator.EvaluateJobContainer(message.JobContainer, jobContext.ExpressionValues);
|
||||
var container = templateEvaluator.EvaluateJobContainer(message.JobContainer, jobContext.ExpressionValues, jobContext.ExpressionFunctions);
|
||||
if (container != null)
|
||||
{
|
||||
jobContext.Container = new Container.ContainerInfo(HostContext, container);
|
||||
@@ -101,7 +162,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// Evaluate the job service containers
|
||||
context.Debug("Evaluating job service containers");
|
||||
var serviceContainers = templateEvaluator.EvaluateJobServiceContainers(message.JobServiceContainers, jobContext.ExpressionValues);
|
||||
var serviceContainers = templateEvaluator.EvaluateJobServiceContainers(message.JobServiceContainers, jobContext.ExpressionValues, jobContext.ExpressionFunctions);
|
||||
if (serviceContainers?.Count > 0)
|
||||
{
|
||||
foreach (var pair in serviceContainers)
|
||||
@@ -112,6 +173,26 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate the job defaults
|
||||
context.Debug("Evaluating job defaults");
|
||||
foreach (var token in message.Defaults)
|
||||
{
|
||||
var defaults = token.AssertMapping("defaults");
|
||||
if (defaults.Any(x => string.Equals(x.Key.AssertString("defaults key").Value, "run", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
context.JobDefaults["run"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var defaultsRun = defaults.First(x => string.Equals(x.Key.AssertString("defaults key").Value, "run", StringComparison.OrdinalIgnoreCase));
|
||||
var jobDefaults = templateEvaluator.EvaluateJobDefaultsRun(defaultsRun.Value, jobContext.ExpressionValues, jobContext.ExpressionFunctions);
|
||||
foreach (var pair in jobDefaults)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(pair.Value))
|
||||
{
|
||||
context.JobDefaults["run"][pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build up 2 lists of steps, pre-job, job
|
||||
// Download actions not already in the cache
|
||||
Trace.Info("Downloading actions");
|
||||
@@ -244,6 +325,58 @@ namespace GitHub.Runner.Worker
|
||||
context.Start();
|
||||
context.Debug("Starting: Complete job");
|
||||
|
||||
// Evaluate job outputs
|
||||
if (message.JobOutputs != null && message.JobOutputs.Type != TokenType.Null)
|
||||
{
|
||||
try
|
||||
{
|
||||
context.Output($"Evaluate and set job outputs");
|
||||
|
||||
// Populate env context for each step
|
||||
Trace.Info("Initialize Env context for evaluating job outputs");
|
||||
#if OS_WINDOWS
|
||||
var envContext = new DictionaryContextData();
|
||||
#else
|
||||
var envContext = new CaseSensitiveDictionaryContextData();
|
||||
#endif
|
||||
context.ExpressionValues["env"] = envContext;
|
||||
foreach (var pair in context.EnvironmentVariables)
|
||||
{
|
||||
envContext[pair.Key] = new StringContextData(pair.Value ?? string.Empty);
|
||||
}
|
||||
|
||||
Trace.Info("Initialize steps context for evaluating job outputs");
|
||||
context.ExpressionValues["steps"] = context.StepsContext.GetScope(context.ScopeName);
|
||||
|
||||
var templateEvaluator = context.ToPipelineTemplateEvaluator();
|
||||
var outputs = templateEvaluator.EvaluateJobOutput(message.JobOutputs, context.ExpressionValues, context.ExpressionFunctions);
|
||||
foreach (var output in outputs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(output.Value))
|
||||
{
|
||||
context.Debug($"Skip output '{output.Key}' since it's empty");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(output.Value, HostContext.SecretMasker.MaskSecrets(output.Value)))
|
||||
{
|
||||
context.Warning($"Skip output '{output.Key}' since it may contain secret.");
|
||||
continue;
|
||||
}
|
||||
|
||||
context.Output($"Set output '{output.Key}'");
|
||||
jobContext.JobOutputs[output.Key] = output.Value;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Result = TaskResult.Failed;
|
||||
context.Error($"Fail to evaluate job outputs");
|
||||
context.Error(ex);
|
||||
jobContext.Result = TaskResultUtil.MergeTaskResults(jobContext.Result, TaskResult.Failed);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.Variables.GetBoolean(Constants.Variables.Actions.RunnerDebug) ?? false)
|
||||
{
|
||||
Trace.Info("Support log upload starting.");
|
||||
|
||||
@@ -107,6 +107,11 @@ namespace GitHub.Runner.Worker
|
||||
return await CompleteJobAsync(jobServer, jobContext, message, TaskResult.Failed);
|
||||
}
|
||||
|
||||
if (jobContext.WriteDebug)
|
||||
{
|
||||
jobContext.SetRunnerContext("debug", "1");
|
||||
}
|
||||
|
||||
jobContext.SetRunnerContext("os", VarUtil.OS);
|
||||
|
||||
string toolsDirectory = HostContext.GetDirectory(WellKnownDirectory.Tools);
|
||||
@@ -226,7 +231,7 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
Trace.Info("Raising job completed event.");
|
||||
var jobCompletedEvent = new JobCompletedEvent(message.RequestId, message.JobId, result);
|
||||
var jobCompletedEvent = new JobCompletedEvent(message.RequestId, message.JobId, result, jobContext.JobOutputs);
|
||||
|
||||
var completeJobRetryLimit = 5;
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
@@ -56,13 +56,22 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
public void SetResult(
|
||||
public void SetConclusion(
|
||||
string scopeName,
|
||||
string stepName,
|
||||
string result)
|
||||
string conclusion)
|
||||
{
|
||||
var step = GetStep(scopeName, stepName);
|
||||
step["result"] = new StringContextData(result);
|
||||
step["conclusion"] = new StringContextData(conclusion);
|
||||
}
|
||||
|
||||
public void SetOutcome(
|
||||
string scopeName,
|
||||
string stepName,
|
||||
string outcome)
|
||||
{
|
||||
var step = GetStep(scopeName, stepName);
|
||||
step["outcome"] = new StringContextData(outcome);
|
||||
}
|
||||
|
||||
private DictionaryContextData GetStep(string scopeName, string stepName)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
@@ -10,8 +8,13 @@ using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Expressions;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
@@ -63,11 +66,7 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
var step = jobContext.JobSteps.Dequeue();
|
||||
IStep nextStep = null;
|
||||
if (jobContext.JobSteps.Count > 0)
|
||||
{
|
||||
nextStep = jobContext.JobSteps.Peek();
|
||||
}
|
||||
var nextStep = jobContext.JobSteps.Count > 0 ? jobContext.JobSteps.Peek() : null;
|
||||
|
||||
Trace.Info($"Processing step: DisplayName='{step.DisplayName}'");
|
||||
ArgUtil.NotNull(step.ExecutionContext, nameof(step.ExecutionContext));
|
||||
@@ -76,16 +75,43 @@ namespace GitHub.Runner.Worker
|
||||
// Start
|
||||
step.ExecutionContext.Start();
|
||||
|
||||
// Set GITHUB_ACTION
|
||||
if (step is IActionRunner actionStep)
|
||||
{
|
||||
step.ExecutionContext.SetGitHubContext("action", actionStep.Action.Name);
|
||||
}
|
||||
// Expression functions
|
||||
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<AlwaysFunction>(PipelineTemplateConstants.Always, 0, 0));
|
||||
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<CancelledFunction>(PipelineTemplateConstants.Cancelled, 0, 0));
|
||||
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<FailureFunction>(PipelineTemplateConstants.Failure, 0, 0));
|
||||
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<SuccessFunction>(PipelineTemplateConstants.Success, 0, 0));
|
||||
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<HashFilesFunction>(PipelineTemplateConstants.HashFiles, 1, byte.MaxValue));
|
||||
|
||||
// Initialize scope
|
||||
if (InitializeScope(step, scopeInputs))
|
||||
{
|
||||
var expressionManager = HostContext.GetService<IExpressionManager>();
|
||||
// Populate env context for each step
|
||||
Trace.Info("Initialize Env context for step");
|
||||
#if OS_WINDOWS
|
||||
var envContext = new DictionaryContextData();
|
||||
#else
|
||||
var envContext = new CaseSensitiveDictionaryContextData();
|
||||
#endif
|
||||
step.ExecutionContext.ExpressionValues["env"] = envContext;
|
||||
foreach (var pair in step.ExecutionContext.EnvironmentVariables)
|
||||
{
|
||||
envContext[pair.Key] = new StringContextData(pair.Value ?? string.Empty);
|
||||
}
|
||||
|
||||
if (step is IActionRunner actionStep)
|
||||
{
|
||||
// Set GITHUB_ACTION
|
||||
step.ExecutionContext.SetGitHubContext("action", actionStep.Action.Name);
|
||||
|
||||
// Evaluate and merge action's env block to env context
|
||||
var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator();
|
||||
var actionEnvironment = templateEvaluator.EvaluateStepEnvironment(actionStep.Action.Environment, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, VarUtil.EnvironmentVariableKeyComparer);
|
||||
foreach (var env in actionEnvironment)
|
||||
{
|
||||
envContext[env.Key] = new StringContextData(env.Value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Register job cancellation call back only if job cancellation token not been fire before each step run
|
||||
@@ -99,28 +125,29 @@ namespace GitHub.Runner.Worker
|
||||
jobContext.JobContext.Status = jobContext.Result?.ToActionResult();
|
||||
|
||||
step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'.");
|
||||
ConditionResult conditionReTestResult;
|
||||
var conditionReTestTraceWriter = new ConditionTraceWriter(Trace, null); // host tracing only
|
||||
var conditionReTestResult = false;
|
||||
if (HostContext.RunnerShutdownToken.IsCancellationRequested)
|
||||
{
|
||||
step.ExecutionContext.Debug($"Skip Re-evaluate condition on runner shutdown.");
|
||||
conditionReTestResult = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
conditionReTestResult = expressionManager.Evaluate(step.ExecutionContext, step.Condition, hostTracingOnly: true);
|
||||
var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator(conditionReTestTraceWriter);
|
||||
var condition = new BasicExpressionToken(null, null, null, step.Condition);
|
||||
conditionReTestResult = templateEvaluator.EvaluateStepIf(condition, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, step.ExecutionContext.ToExpressionState());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Cancel the step since we get exception while re-evaluate step condition.
|
||||
Trace.Info("Caught exception from expression when re-test condition on job cancellation.");
|
||||
step.ExecutionContext.Error(ex);
|
||||
conditionReTestResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!conditionReTestResult.Value)
|
||||
if (!conditionReTestResult)
|
||||
{
|
||||
// Cancel the step.
|
||||
Trace.Info("Cancel current running step.");
|
||||
@@ -140,34 +167,35 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// Evaluate condition.
|
||||
step.ExecutionContext.Debug($"Evaluating condition for step: '{step.DisplayName}'");
|
||||
Exception conditionEvaluateError = null;
|
||||
ConditionResult conditionResult;
|
||||
var conditionTraceWriter = new ConditionTraceWriter(Trace, step.ExecutionContext);
|
||||
var conditionResult = false;
|
||||
var conditionEvaluateError = default(Exception);
|
||||
if (HostContext.RunnerShutdownToken.IsCancellationRequested)
|
||||
{
|
||||
step.ExecutionContext.Debug($"Skip evaluate condition on runner shutdown.");
|
||||
conditionResult = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
conditionResult = expressionManager.Evaluate(step.ExecutionContext, step.Condition);
|
||||
var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator(conditionTraceWriter);
|
||||
var condition = new BasicExpressionToken(null, null, null, step.Condition);
|
||||
conditionResult = templateEvaluator.EvaluateStepIf(condition, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, step.ExecutionContext.ToExpressionState());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Info("Caught exception from expression.");
|
||||
Trace.Error(ex);
|
||||
conditionResult = false;
|
||||
conditionEvaluateError = ex;
|
||||
}
|
||||
}
|
||||
|
||||
// no evaluate error but condition is false
|
||||
if (!conditionResult.Value && conditionEvaluateError == null)
|
||||
if (!conditionResult && conditionEvaluateError == null)
|
||||
{
|
||||
// Condition == false
|
||||
Trace.Info("Skipping step due to condition evaluation.");
|
||||
CompleteStep(step, nextStep, TaskResult.Skipped, resultCode: conditionResult.Trace);
|
||||
CompleteStep(step, nextStep, TaskResult.Skipped, resultCode: conditionTraceWriter.Trace);
|
||||
}
|
||||
else if (conditionEvaluateError != null)
|
||||
{
|
||||
@@ -224,10 +252,10 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// Set the timeout
|
||||
var timeoutMinutes = 0;
|
||||
var templateEvaluator = CreateTemplateEvaluator(step.ExecutionContext);
|
||||
var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator();
|
||||
try
|
||||
{
|
||||
timeoutMinutes = templateEvaluator.EvaluateStepTimeout(step.Timeout, step.ExecutionContext.ExpressionValues);
|
||||
timeoutMinutes = templateEvaluator.EvaluateStepTimeout(step.Timeout, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -318,7 +346,7 @@ namespace GitHub.Runner.Worker
|
||||
var continueOnError = false;
|
||||
try
|
||||
{
|
||||
continueOnError = templateEvaluator.EvaluateStepContinueOnError(step.ContinueOnError, step.ExecutionContext.ExpressionValues);
|
||||
continueOnError = templateEvaluator.EvaluateStepContinueOnError(step.ContinueOnError, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -330,6 +358,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
if (continueOnError)
|
||||
{
|
||||
step.ExecutionContext.Outcome = step.ExecutionContext.Result;
|
||||
step.ExecutionContext.Result = TaskResult.Succeeded;
|
||||
Trace.Info($"Updated step result (continue on error)");
|
||||
}
|
||||
@@ -366,11 +395,11 @@ namespace GitHub.Runner.Worker
|
||||
executionContext.Debug($"Initializing scope '{scope.Name}'");
|
||||
executionContext.ExpressionValues["steps"] = stepsContext.GetScope(scope.ParentName);
|
||||
executionContext.ExpressionValues["inputs"] = !String.IsNullOrEmpty(scope.ParentName) ? scopeInputs[scope.ParentName] : null;
|
||||
var templateEvaluator = CreateTemplateEvaluator(executionContext);
|
||||
var templateEvaluator = executionContext.ToPipelineTemplateEvaluator();
|
||||
var inputs = default(DictionaryContextData);
|
||||
try
|
||||
{
|
||||
inputs = templateEvaluator.EvaluateStepScopeInputs(scope.Inputs, executionContext.ExpressionValues);
|
||||
inputs = templateEvaluator.EvaluateStepScopeInputs(scope.Inputs, executionContext.ExpressionValues, executionContext.ExpressionFunctions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -422,11 +451,11 @@ namespace GitHub.Runner.Worker
|
||||
executionContext.Debug($"Finalizing scope '{scope.Name}'");
|
||||
executionContext.ExpressionValues["steps"] = stepsContext.GetScope(scope.Name);
|
||||
executionContext.ExpressionValues["inputs"] = null;
|
||||
var templateEvaluator = CreateTemplateEvaluator(executionContext);
|
||||
var templateEvaluator = executionContext.ToPipelineTemplateEvaluator();
|
||||
var outputs = default(DictionaryContextData);
|
||||
try
|
||||
{
|
||||
outputs = templateEvaluator.EvaluateStepScopeOutputs(scope.Outputs, executionContext.ExpressionValues);
|
||||
outputs = templateEvaluator.EvaluateStepScopeOutputs(scope.Outputs, executionContext.ExpressionValues, executionContext.ExpressionFunctions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -455,11 +484,42 @@ namespace GitHub.Runner.Worker
|
||||
executionContext.Complete(result, resultCode: resultCode);
|
||||
}
|
||||
|
||||
private PipelineTemplateEvaluator CreateTemplateEvaluator(IExecutionContext executionContext)
|
||||
private sealed class ConditionTraceWriter : ObjectTemplating::ITraceWriter
|
||||
{
|
||||
var templateTrace = executionContext.ToTemplateTraceWriter();
|
||||
var schema = new PipelineTemplateSchemaFactory().CreateSchema();
|
||||
return new PipelineTemplateEvaluator(templateTrace, schema);
|
||||
private readonly IExecutionContext _executionContext;
|
||||
private readonly Tracing _trace;
|
||||
private readonly StringBuilder _traceBuilder = new StringBuilder();
|
||||
|
||||
public string Trace => _traceBuilder.ToString();
|
||||
|
||||
public ConditionTraceWriter(Tracing trace, IExecutionContext executionContext)
|
||||
{
|
||||
ArgUtil.NotNull(trace, nameof(trace));
|
||||
_trace = trace;
|
||||
_executionContext = executionContext;
|
||||
}
|
||||
|
||||
public void Error(string format, params Object[] args)
|
||||
{
|
||||
var message = StringUtil.Format(format, args);
|
||||
_trace.Error(message);
|
||||
_executionContext?.Debug(message);
|
||||
}
|
||||
|
||||
public void Info(string format, params Object[] args)
|
||||
{
|
||||
var message = StringUtil.Format(format, args);
|
||||
_trace.Info(message);
|
||||
_executionContext?.Debug(message);
|
||||
_traceBuilder.AppendLine(message);
|
||||
}
|
||||
|
||||
public void Verbose(string format, params Object[] args)
|
||||
{
|
||||
var message = StringUtil.Format(format, args);
|
||||
_trace.Verbose(message);
|
||||
_executionContext?.Debug(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +90,9 @@
|
||||
"github",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace GitHub.DistributedTask.Expressions2
|
||||
AddFunction<Join>("join", 1, 2);
|
||||
AddFunction<StartsWith>("startsWith", 2, 2);
|
||||
AddFunction<ToJson>("toJson", 1, 1);
|
||||
AddFunction<HashFiles>("hashFiles", 1, 1);
|
||||
AddFunction<FromJson>("fromJson", 1, 1);
|
||||
}
|
||||
|
||||
private static void AddFunction<T>(String name, Int32 minParameters, Int32 maxParameters)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
|
||||
{
|
||||
internal sealed class FromJson : Function
|
||||
{
|
||||
protected sealed override Object EvaluateCore(
|
||||
EvaluationContext context,
|
||||
out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var json = Parameters[0].Evaluate(context).ConvertToString();
|
||||
using (var stringReader = new StringReader(json))
|
||||
using (var jsonReader = new JsonTextReader(stringReader) { DateParseHandling = DateParseHandling.None, FloatParseHandling = FloatParseHandling.Double })
|
||||
{
|
||||
var token = JToken.ReadFrom(jsonReader);
|
||||
return token.ToPipelineContextData();
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -1,122 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Minimatch;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
|
||||
{
|
||||
internal sealed class HashFiles : Function
|
||||
{
|
||||
protected sealed override Object EvaluateCore(
|
||||
EvaluationContext context,
|
||||
out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
|
||||
// hashFiles() only works on the runner and only works with files under GITHUB_WORKSPACE
|
||||
// Since GITHUB_WORKSPACE is set by runner, I am using that as the fact of this code runs on server or runner.
|
||||
if (context.State is ObjectTemplating.TemplateContext templateContext &&
|
||||
templateContext.ExpressionValues.TryGetValue(PipelineTemplateConstants.GitHub, out var githubContextData) &&
|
||||
githubContextData is DictionaryContextData githubContext &&
|
||||
githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out var workspace) == true &&
|
||||
workspace is StringContextData workspaceData)
|
||||
{
|
||||
string searchRoot = workspaceData.Value;
|
||||
string pattern = Parameters[0].Evaluate(context).ConvertToString();
|
||||
|
||||
// Convert slashes on Windows
|
||||
if (s_isWindows)
|
||||
{
|
||||
pattern = pattern.Replace('\\', '/');
|
||||
}
|
||||
|
||||
// Root the pattern
|
||||
if (!Path.IsPathRooted(pattern))
|
||||
{
|
||||
var patternRoot = s_isWindows ? searchRoot.Replace('\\', '/').TrimEnd('/') : searchRoot.TrimEnd('/');
|
||||
pattern = string.Concat(patternRoot, "/", pattern);
|
||||
}
|
||||
|
||||
// Get all files
|
||||
context.Trace.Info($"Search root directory: '{searchRoot}'");
|
||||
context.Trace.Info($"Search pattern: '{pattern}'");
|
||||
var files = Directory.GetFiles(searchRoot, "*", SearchOption.AllDirectories)
|
||||
.Select(x => s_isWindows ? x.Replace('\\', '/') : x)
|
||||
.OrderBy(x => x, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
if (files.Count == 0)
|
||||
{
|
||||
throw new ArgumentException($"hashFiles('{ExpressionUtility.StringEscape(pattern)}') failed. Directory '{searchRoot}' is empty");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Trace.Info($"Found {files.Count} files");
|
||||
}
|
||||
|
||||
// Match
|
||||
var matcher = new Minimatcher(pattern, s_minimatchOptions);
|
||||
files = matcher.Filter(files)
|
||||
.Select(x => s_isWindows ? x.Replace('/', '\\') : x)
|
||||
.ToList();
|
||||
if (files.Count == 0)
|
||||
{
|
||||
throw new ArgumentException($"hashFiles('{ExpressionUtility.StringEscape(pattern)}') failed. Search pattern '{pattern}' doesn't match any file under '{searchRoot}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Trace.Info($"{files.Count} matches to hash");
|
||||
}
|
||||
|
||||
// Hash each file
|
||||
List<byte> filesSha256 = new List<byte>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
context.Trace.Info($"Hash {file}");
|
||||
using (SHA256 sha256hash = SHA256.Create())
|
||||
{
|
||||
using (var fileStream = File.OpenRead(file))
|
||||
{
|
||||
filesSha256.AddRange(sha256hash.ComputeHash(fileStream));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the hashes
|
||||
using (SHA256 sha256hash = SHA256.Create())
|
||||
{
|
||||
var hashBytes = sha256hash.ComputeHash(filesSha256.ToArray());
|
||||
StringBuilder hashString = new StringBuilder();
|
||||
for (int i = 0; i < hashBytes.Length; i++)
|
||||
{
|
||||
hashString.Append(hashBytes[i].ToString("x2"));
|
||||
}
|
||||
var result = hashString.ToString();
|
||||
context.Trace.Info($"Final hash result: '{result}'");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("'hashfiles' expression function is only supported under runner context.");
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly bool s_isWindows = Environment.OSVersion.Platform != PlatformID.Unix && Environment.OSVersion.Platform != PlatformID.MacOSX;
|
||||
|
||||
// Only support basic globbing (* ? and []) and globstar (**)
|
||||
private static readonly Options s_minimatchOptions = new Options
|
||||
{
|
||||
Dot = true,
|
||||
NoBrace = true,
|
||||
NoCase = s_isWindows,
|
||||
NoComment = true,
|
||||
NoExt = true,
|
||||
NoNegate = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -779,5 +779,65 @@ namespace GitHub.DistributedTask.WebApi
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Preview API]
|
||||
/// </summary>
|
||||
/// <param name="poolId"></param>
|
||||
/// <param name="agentId"></param>
|
||||
/// <param name="userState"></param>
|
||||
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
|
||||
public Task<String> GetAgentAuthUrlAsync(
|
||||
int poolId,
|
||||
int agentId,
|
||||
object userState = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpMethod httpMethod = new HttpMethod("GET");
|
||||
Guid locationId = new Guid("a82a119c-1e46-44b6-8d75-c82a79cf975b");
|
||||
object routeValues = new { poolId = poolId, agentId = agentId };
|
||||
|
||||
return SendAsync<String>(
|
||||
httpMethod,
|
||||
locationId,
|
||||
routeValues: routeValues,
|
||||
version: new ApiResourceVersion(6.0, 1),
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [Preview API]
|
||||
/// </summary>
|
||||
/// <param name="poolId"></param>
|
||||
/// <param name="agentId"></param>
|
||||
/// <param name="error"></param>
|
||||
/// <param name="userState"></param>
|
||||
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public virtual async Task ReportAgentAuthUrlMigrationErrorAsync(
|
||||
int poolId,
|
||||
int agentId,
|
||||
string error,
|
||||
object userState = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpMethod httpMethod = new HttpMethod("POST");
|
||||
Guid locationId = new Guid("a82a119c-1e46-44b6-8d75-c82a79cf975b");
|
||||
object routeValues = new { poolId = poolId, agentId = agentId };
|
||||
HttpContent content = new ObjectContent<string>(error, new VssJsonMediaTypeFormatter(true));
|
||||
|
||||
using (HttpResponseMessage response = await SendAsync(
|
||||
httpMethod,
|
||||
locationId,
|
||||
routeValues: routeValues,
|
||||
version: new ApiResourceVersion(6.0, 1),
|
||||
userState: userState,
|
||||
cancellationToken: cancellationToken,
|
||||
content: content).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
|
||||
@@ -22,10 +23,27 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
{
|
||||
var context = definition[i].Value.AssertSequence($"{TemplateConstants.Context}");
|
||||
definition.RemoveAt(i);
|
||||
Context = context
|
||||
.Select(x => x.AssertString($"{TemplateConstants.Context} item").Value)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
var readerContext = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
var evaluatorContext = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (TemplateToken item in context)
|
||||
{
|
||||
var itemStr = item.AssertString($"{TemplateConstants.Context} item").Value;
|
||||
readerContext.Add(itemStr);
|
||||
|
||||
// Remove min/max parameter info
|
||||
var paramIndex = itemStr.IndexOf('(');
|
||||
if (paramIndex > 0)
|
||||
{
|
||||
evaluatorContext.Add(String.Concat(itemStr.Substring(0, paramIndex + 1), ")"));
|
||||
}
|
||||
else
|
||||
{
|
||||
evaluatorContext.Add(itemStr);
|
||||
}
|
||||
}
|
||||
|
||||
ReaderContext = readerContext.ToArray();
|
||||
EvaluatorContext = evaluatorContext.ToArray();
|
||||
}
|
||||
else if (String.Equals(definitionKey.Value, TemplateConstants.Description, StringComparison.Ordinal))
|
||||
{
|
||||
@@ -40,7 +58,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
|
||||
internal abstract DefinitionType DefinitionType { get; }
|
||||
|
||||
internal String[] Context { get; private set; } = new String[0];
|
||||
/// <summary>
|
||||
/// Used by the template reader to determine allowed expression values and functions.
|
||||
/// Also used by the template reader to validate function min/max parameters.
|
||||
/// </summary>
|
||||
internal String[] ReaderContext { get; private set; } = new String[0];
|
||||
|
||||
/// <summary>
|
||||
/// Used by the template evaluator to determine allowed expression values and functions.
|
||||
/// The min/max parameter info is omitted.
|
||||
/// </summary>
|
||||
internal String[] EvaluatorContext { get; private set; } = new String[0];
|
||||
|
||||
internal abstract void Validate(
|
||||
TemplateSchema schema,
|
||||
|
||||
@@ -30,8 +30,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
foreach (var propertiesPair in properties)
|
||||
{
|
||||
var propertyName = propertiesPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.Properties} key");
|
||||
var propertyValue = propertiesPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.Properties} value");
|
||||
Properties.Add(propertyName.Value, new PropertyValue(propertyValue.Value));
|
||||
Properties.Add(propertyName.Value, new PropertyValue(propertiesPair.Value));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -85,7 +84,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Property '{TemplateConstants.LooseKeyType}' is defined but '{TemplateConstants.LooseValueType}' is not defined");
|
||||
throw new ArgumentException($"Property '{TemplateConstants.LooseKeyType}' is defined but '{TemplateConstants.LooseValueType}' is not defined on '{name}'");
|
||||
}
|
||||
}
|
||||
// Otherwise validate loose value type not be defined
|
||||
@@ -95,16 +94,21 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
}
|
||||
|
||||
// Lookup each property
|
||||
foreach (var property in Properties.Values)
|
||||
foreach (var property in Properties)
|
||||
{
|
||||
schema.GetDefinition(property.Type);
|
||||
if (String.IsNullOrEmpty(property.Value.Type))
|
||||
{
|
||||
throw new ArgumentException($"Type not specified for the '{property.Key}' property on the '{name}' type");
|
||||
}
|
||||
|
||||
schema.GetDefinition(property.Value.Type);
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(Inherits))
|
||||
{
|
||||
var inherited = schema.GetDefinition(Inherits);
|
||||
|
||||
if (inherited.Context.Length > 0)
|
||||
if (inherited.ReaderContext.Length > 0)
|
||||
{
|
||||
throw new NotSupportedException($"Property '{TemplateConstants.Context}' is not supported on inhertied definitions");
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
{
|
||||
var nestedDefinition = schema.GetDefinition(nestedType);
|
||||
|
||||
if (nestedDefinition.Context.Length > 0)
|
||||
if (nestedDefinition.ReaderContext.Length > 0)
|
||||
{
|
||||
throw new ArgumentException($"'{name}' is a one-of definition and references another definition that defines context. This is currently not supported.");
|
||||
}
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
using System;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
|
||||
namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
{
|
||||
internal sealed class PropertyValue
|
||||
{
|
||||
internal PropertyValue()
|
||||
internal PropertyValue(TemplateToken token)
|
||||
{
|
||||
if (token is StringToken stringToken)
|
||||
{
|
||||
Type = stringToken.Value;
|
||||
}
|
||||
|
||||
internal PropertyValue(String type)
|
||||
else
|
||||
{
|
||||
Type = type;
|
||||
var mapping = token.AssertMapping($"{TemplateConstants.MappingPropertyValue}");
|
||||
foreach (var mappingPair in mapping)
|
||||
{
|
||||
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.MappingPropertyValue} key");
|
||||
switch (mappingKey.Value)
|
||||
{
|
||||
case TemplateConstants.Type:
|
||||
Type = mappingPair.Value.AssertString($"{TemplateConstants.MappingPropertyValue} {TemplateConstants.Type}").Value;
|
||||
break;
|
||||
case TemplateConstants.Required:
|
||||
Required = mappingPair.Value.AssertBoolean($"{TemplateConstants.MappingPropertyValue} {TemplateConstants.Required}").Value;
|
||||
break;
|
||||
default:
|
||||
mappingKey.AssertUnexpectedValue($"{TemplateConstants.MappingPropertyValue} key");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal String Type { get; set; }
|
||||
|
||||
internal Boolean Required { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,8 +312,8 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
|
||||
// template-schema
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Version, new PropertyValue(TemplateConstants.NonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Definitions, new PropertyValue(TemplateConstants.Definitions));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Version, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Definitions, new PropertyValue(new StringToken(null, null, null, TemplateConstants.Definitions)));
|
||||
schema.Definitions.Add(TemplateConstants.TemplateSchema, mappingDefinition);
|
||||
|
||||
// definitions
|
||||
@@ -335,9 +335,9 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
|
||||
// null-definition
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(TemplateConstants.String));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(TemplateConstants.SequenceOfNonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Null, new PropertyValue(TemplateConstants.NullDefinitionProperties));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Null, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NullDefinitionProperties)));
|
||||
schema.Definitions.Add(TemplateConstants.NullDefinition, mappingDefinition);
|
||||
|
||||
// null-definition-properties
|
||||
@@ -346,9 +346,9 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
|
||||
// boolean-definition
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(TemplateConstants.String));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(TemplateConstants.SequenceOfNonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Boolean, new PropertyValue(TemplateConstants.BooleanDefinitionProperties));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Boolean, new PropertyValue(new StringToken(null, null, null, TemplateConstants.BooleanDefinitionProperties)));
|
||||
schema.Definitions.Add(TemplateConstants.BooleanDefinition, mappingDefinition);
|
||||
|
||||
// boolean-definition-properties
|
||||
@@ -357,9 +357,9 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
|
||||
// number-definition
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(TemplateConstants.String));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(TemplateConstants.SequenceOfNonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Number, new PropertyValue(TemplateConstants.NumberDefinitionProperties));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Number, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NumberDefinitionProperties)));
|
||||
schema.Definitions.Add(TemplateConstants.NumberDefinition, mappingDefinition);
|
||||
|
||||
// number-definition-properties
|
||||
@@ -368,55 +368,68 @@ namespace GitHub.DistributedTask.ObjectTemplating.Schema
|
||||
|
||||
// string-definition
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(TemplateConstants.String));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(TemplateConstants.SequenceOfNonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.String, new PropertyValue(TemplateConstants.StringDefinitionProperties));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.String, new PropertyValue(new StringToken(null, null, null, TemplateConstants.StringDefinitionProperties)));
|
||||
schema.Definitions.Add(TemplateConstants.StringDefinition, mappingDefinition);
|
||||
|
||||
// string-definition-properties
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Constant, new PropertyValue(TemplateConstants.NonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.IgnoreCase, new PropertyValue(TemplateConstants.Boolean));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.RequireNonEmpty, new PropertyValue(TemplateConstants.Boolean));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Constant, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.IgnoreCase, new PropertyValue(new StringToken(null, null, null,TemplateConstants.Boolean)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.RequireNonEmpty, new PropertyValue(new StringToken(null, null, null, TemplateConstants.Boolean)));
|
||||
schema.Definitions.Add(TemplateConstants.StringDefinitionProperties, mappingDefinition);
|
||||
|
||||
// sequence-definition
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(TemplateConstants.String));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(TemplateConstants.SequenceOfNonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Sequence, new PropertyValue(TemplateConstants.SequenceDefinitionProperties));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Sequence, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceDefinitionProperties)));
|
||||
schema.Definitions.Add(TemplateConstants.SequenceDefinition, mappingDefinition);
|
||||
|
||||
// sequence-definition-properties
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.ItemType, new PropertyValue(TemplateConstants.NonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.ItemType, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
|
||||
schema.Definitions.Add(TemplateConstants.SequenceDefinitionProperties, mappingDefinition);
|
||||
|
||||
// mapping-definition
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(TemplateConstants.String));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(TemplateConstants.SequenceOfNonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Mapping, new PropertyValue(TemplateConstants.MappingDefinitionProperties));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Mapping, new PropertyValue(new StringToken(null, null, null, TemplateConstants.MappingDefinitionProperties)));
|
||||
schema.Definitions.Add(TemplateConstants.MappingDefinition, mappingDefinition);
|
||||
|
||||
// mapping-definition-properties
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Properties, new PropertyValue(TemplateConstants.Properties));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.LooseKeyType, new PropertyValue(TemplateConstants.NonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.LooseValueType, new PropertyValue(TemplateConstants.NonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Properties, new PropertyValue(new StringToken(null, null, null, TemplateConstants.Properties)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.LooseKeyType, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.LooseValueType, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
|
||||
schema.Definitions.Add(TemplateConstants.MappingDefinitionProperties, mappingDefinition);
|
||||
|
||||
// properties
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.LooseKeyType = TemplateConstants.NonEmptyString;
|
||||
mappingDefinition.LooseValueType = TemplateConstants.NonEmptyString;
|
||||
mappingDefinition.LooseValueType = TemplateConstants.PropertyValue;
|
||||
schema.Definitions.Add(TemplateConstants.Properties, mappingDefinition);
|
||||
|
||||
// property-value
|
||||
oneOfDefinition = new OneOfDefinition();
|
||||
oneOfDefinition.OneOf.Add(TemplateConstants.NonEmptyString);
|
||||
oneOfDefinition.OneOf.Add(TemplateConstants.MappingPropertyValue);
|
||||
schema.Definitions.Add(TemplateConstants.PropertyValue, oneOfDefinition);
|
||||
|
||||
// mapping-property-value
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Type, new PropertyValue(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Required, new PropertyValue(new StringToken(null, null, null, TemplateConstants.Boolean)));
|
||||
schema.Definitions.Add(TemplateConstants.MappingPropertyValue, mappingDefinition);
|
||||
|
||||
|
||||
// one-of-definition
|
||||
mappingDefinition = new MappingDefinition();
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(TemplateConstants.String));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(TemplateConstants.SequenceOfNonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.OneOf, new PropertyValue(TemplateConstants.SequenceOfNonEmptyString));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyValue(new StringToken(null, null, null, TemplateConstants.String)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
|
||||
mappingDefinition.Properties.Add(TemplateConstants.OneOf, new PropertyValue(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
|
||||
schema.Definitions.Add(TemplateConstants.OneOfDefinition, mappingDefinition);
|
||||
|
||||
// non-empty-string
|
||||
|
||||
@@ -22,9 +22,11 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
internal const String ItemType = "item-type";
|
||||
internal const String LooseKeyType = "loose-key-type";
|
||||
internal const String LooseValueType = "loose-value-type";
|
||||
internal const String MaxConstant = "MAX";
|
||||
internal const String Mapping = "mapping";
|
||||
internal const String MappingDefinition = "mapping-definition";
|
||||
internal const String MappingDefinitionProperties = "mapping-definition-properties";
|
||||
internal const String MappingPropertyValue = "mapping-property-value";
|
||||
internal const String NonEmptyString = "non-empty-string";
|
||||
internal const String Null = "null";
|
||||
internal const String NullDefinition = "null-definition";
|
||||
@@ -35,7 +37,9 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
internal const String OneOf = "one-of";
|
||||
internal const String OneOfDefinition = "one-of-definition";
|
||||
internal const String OpenExpression = "${{";
|
||||
internal const String PropertyValue = "property-value";
|
||||
internal const String Properties = "properties";
|
||||
internal const String Required = "required";
|
||||
internal const String RequireNonEmpty = "require-non-empty";
|
||||
internal const String Scalar = "scalar";
|
||||
internal const String ScalarDefinition = "scalar-definition";
|
||||
@@ -43,6 +47,7 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
internal const String Sequence = "sequence";
|
||||
internal const String SequenceDefinition = "sequence-definition";
|
||||
internal const String SequenceDefinitionProperties = "sequence-definition-properties";
|
||||
internal const String Type = "type";
|
||||
internal const String SequenceOfNonEmptyString = "sequence-of-non-empty-string";
|
||||
internal const String String = "string";
|
||||
internal const String StringDefinition = "string-definition";
|
||||
|
||||
@@ -184,6 +184,7 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
id = FileIds.Count + 1;
|
||||
FileIds.Add(file, id);
|
||||
FileNames.Add(file);
|
||||
Memory.AddBytes(file);
|
||||
}
|
||||
|
||||
return id;
|
||||
@@ -191,7 +192,12 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
|
||||
internal String GetFileName(Int32 fileId)
|
||||
{
|
||||
return FileNames[fileId - 1];
|
||||
return FileNames.Count >= fileId ? FileNames[fileId - 1] : null;
|
||||
}
|
||||
|
||||
internal IReadOnlyList<String> GetFileTable()
|
||||
{
|
||||
return FileNames.AsReadOnly();
|
||||
}
|
||||
|
||||
private String GetErrorPrefix(
|
||||
@@ -199,9 +205,9 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
Int32? line,
|
||||
Int32? column)
|
||||
{
|
||||
if (fileId != null)
|
||||
var fileName = fileId.HasValue ? GetFileName(fileId.Value) : null;
|
||||
if (!String.IsNullOrEmpty(fileName))
|
||||
{
|
||||
var fileName = GetFileName(fileId.Value);
|
||||
if (line != null && column != null)
|
||||
{
|
||||
return $"{fileName} {TemplateStrings.LineColumn(line, column)}:";
|
||||
|
||||
@@ -47,7 +47,16 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
var evaluator = new TemplateEvaluator(context, template, removeBytes);
|
||||
try
|
||||
{
|
||||
var availableContext = new HashSet<String>(context.ExpressionValues.Keys);
|
||||
var availableContext = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var key in context.ExpressionValues.Keys)
|
||||
{
|
||||
availableContext.Add(key);
|
||||
}
|
||||
foreach (var function in context.ExpressionFunctions)
|
||||
{
|
||||
availableContext.Add($"{function.Name}()");
|
||||
}
|
||||
|
||||
var definitionInfo = new DefinitionInfo(context.Schema, type, availableContext);
|
||||
result = evaluator.Evaluate(definitionInfo);
|
||||
|
||||
@@ -182,12 +191,14 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
}
|
||||
|
||||
var keys = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
var hasExpressionKey = false;
|
||||
|
||||
while (m_unraveler.AllowScalar(definition.Expand, out ScalarToken nextKeyScalar))
|
||||
{
|
||||
// Expression
|
||||
if (nextKeyScalar is ExpressionToken)
|
||||
{
|
||||
hasExpressionKey = true;
|
||||
var anyDefinition = new DefinitionInfo(definition, TemplateConstants.Any);
|
||||
mapping.Add(nextKeyScalar, Evaluate(anyDefinition));
|
||||
continue;
|
||||
@@ -268,6 +279,19 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
String listToDeDuplicate = String.Join(", ", nonDuplicates);
|
||||
m_context.Error(mapping, TemplateStrings.UnableToDetermineOneOf(listToDeDuplicate));
|
||||
}
|
||||
else if (mappingDefinitions.Count == 1 && !hasExpressionKey)
|
||||
{
|
||||
foreach (var property in mappingDefinitions[0].Properties)
|
||||
{
|
||||
if (property.Value.Required)
|
||||
{
|
||||
if (!keys.Contains(property.Key))
|
||||
{
|
||||
m_context.Error(mapping, $"Required property is missing: {property.Key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_unraveler.ReadMappingEnd();
|
||||
}
|
||||
@@ -378,14 +402,13 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
Definition = m_schema.GetDefinition(name);
|
||||
|
||||
// Determine whether to expand
|
||||
if (Definition.Context.Length > 0)
|
||||
m_allowedContext = Definition.EvaluatorContext;
|
||||
if (Definition.EvaluatorContext.Length > 0)
|
||||
{
|
||||
m_allowedContext = Definition.Context;
|
||||
Expand = m_availableContext.IsSupersetOf(m_allowedContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_allowedContext = new String[0];
|
||||
Expand = false;
|
||||
}
|
||||
}
|
||||
@@ -401,9 +424,9 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
Definition = m_schema.GetDefinition(name);
|
||||
|
||||
// Determine whether to expand
|
||||
if (Definition.Context.Length > 0)
|
||||
if (Definition.EvaluatorContext.Length > 0)
|
||||
{
|
||||
m_allowedContext = new HashSet<String>(parent.m_allowedContext.Concat(Definition.Context)).ToArray();
|
||||
m_allowedContext = new HashSet<String>(parent.m_allowedContext.Concat(Definition.EvaluatorContext), StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
Expand = m_availableContext.IsSupersetOf(m_allowedContext);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -49,6 +49,14 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
m_errors = new List<TemplateValidationError>(errors ?? Enumerable.Empty<TemplateValidationError>());
|
||||
}
|
||||
|
||||
public TemplateValidationException(
|
||||
String message,
|
||||
IEnumerable<TemplateValidationError> errors)
|
||||
: this(message)
|
||||
{
|
||||
m_errors = new List<TemplateValidationError>(errors ?? Enumerable.Empty<TemplateValidationError>());
|
||||
}
|
||||
|
||||
public TemplateValidationException(String message)
|
||||
: base(message)
|
||||
{
|
||||
|
||||
@@ -178,14 +178,15 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
}
|
||||
|
||||
var keys = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
var hasExpressionKey = false;
|
||||
|
||||
while (m_objectReader.AllowLiteral(out LiteralToken rawLiteral))
|
||||
{
|
||||
var nextKeyScalar = ParseScalar(rawLiteral, definition.AllowedContext);
|
||||
|
||||
// Expression
|
||||
if (nextKeyScalar is ExpressionToken)
|
||||
{
|
||||
hasExpressionKey = true;
|
||||
// Legal
|
||||
if (definition.AllowedContext.Length > 0)
|
||||
{
|
||||
@@ -280,7 +281,19 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
String listToDeDuplicate = String.Join(", ", nonDuplicates);
|
||||
m_context.Error(mapping, TemplateStrings.UnableToDetermineOneOf(listToDeDuplicate));
|
||||
}
|
||||
|
||||
else if (mappingDefinitions.Count == 1 && !hasExpressionKey)
|
||||
{
|
||||
foreach (var property in mappingDefinitions[0].Properties)
|
||||
{
|
||||
if (property.Value.Required)
|
||||
{
|
||||
if (!keys.Contains(property.Key))
|
||||
{
|
||||
m_context.Error(mapping, $"Required property is missing: {property.Key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ExpectMappingEnd();
|
||||
}
|
||||
|
||||
@@ -767,15 +780,8 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
// Lookup the definition
|
||||
Definition = m_schema.GetDefinition(name);
|
||||
|
||||
// Determine whether to expand
|
||||
if (Definition.Context.Length > 0)
|
||||
{
|
||||
AllowedContext = Definition.Context;
|
||||
}
|
||||
else
|
||||
{
|
||||
AllowedContext = new String[0];
|
||||
}
|
||||
// Record allowed context
|
||||
AllowedContext = Definition.ReaderContext;
|
||||
}
|
||||
|
||||
public DefinitionInfo(
|
||||
@@ -787,10 +793,10 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
// Lookup the definition
|
||||
Definition = m_schema.GetDefinition(name);
|
||||
|
||||
// Determine whether to expand
|
||||
if (Definition.Context.Length > 0)
|
||||
// Record allowed context
|
||||
if (Definition.ReaderContext.Length > 0)
|
||||
{
|
||||
AllowedContext = new HashSet<String>(parent.AllowedContext.Concat(Definition.Context)).ToArray();
|
||||
AllowedContext = new HashSet<String>(parent.AllowedContext.Concat(Definition.ReaderContext), StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.ObjectTemplating
|
||||
@@ -41,7 +42,7 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
{
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
String message = !String.IsNullOrEmpty(messagePrefix) ? $"{messagePrefix} {ex.Message}" : ex.Message;
|
||||
String message = !String.IsNullOrEmpty(messagePrefix) ? $"{messagePrefix} {ex.Message}" : ex.ToString();
|
||||
Add(new TemplateValidationError(message));
|
||||
if (ex.InnerException == null)
|
||||
{
|
||||
@@ -88,6 +89,23 @@ namespace GitHub.DistributedTask.ObjectTemplating
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws <c ref="TemplateValidationException" /> if any errors.
|
||||
/// <param name="prefix">The error message prefix</param>
|
||||
/// </summary>
|
||||
public void Check(String prefix)
|
||||
{
|
||||
if (String.IsNullOrEmpty(prefix))
|
||||
{
|
||||
this.Check();
|
||||
}
|
||||
else if (m_errors.Count > 0)
|
||||
{
|
||||
var message = $"{prefix.Trim()} {String.Join(",", m_errors.Select(e => e.Message))}";
|
||||
throw new TemplateValidationException(message, m_errors);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
m_errors.Clear();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
@@ -35,11 +37,29 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
String[] allowedContext,
|
||||
out Exception ex)
|
||||
{
|
||||
// Create dummy allowed contexts
|
||||
INamedValueInfo[] namedValues = null;
|
||||
// Create dummy named values and functions
|
||||
var namedValues = new List<INamedValueInfo>();
|
||||
var functions = new List<IFunctionInfo>();
|
||||
if (allowedContext?.Length > 0)
|
||||
{
|
||||
namedValues = allowedContext.Select(x => new NamedValueInfo<ContextValueNode>(x)).ToArray();
|
||||
foreach (var contextItem in allowedContext)
|
||||
{
|
||||
var match = s_function.Match(contextItem);
|
||||
if (match.Success)
|
||||
{
|
||||
var functionName = match.Groups[1].Value;
|
||||
var minParameters = Int32.Parse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture);
|
||||
var maxParametersRaw = match.Groups[3].Value;
|
||||
var maxParameters = String.Equals(maxParametersRaw, TemplateConstants.MaxConstant, StringComparison.Ordinal)
|
||||
? Int32.MaxValue
|
||||
: Int32.Parse(maxParametersRaw, NumberStyles.None, CultureInfo.InvariantCulture);
|
||||
functions.Add(new FunctionInfo<DummyFunction>(functionName, minParameters, maxParameters));
|
||||
}
|
||||
else
|
||||
{
|
||||
namedValues.Add(new NamedValueInfo<ContextValueNode>(contextItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse
|
||||
@@ -47,7 +67,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
ExpressionNode root = null;
|
||||
try
|
||||
{
|
||||
root = new ExpressionParser().CreateTree(expression, null, namedValues, null) as ExpressionNode;
|
||||
root = new ExpressionParser().CreateTree(expression, null, namedValues, functions) as ExpressionNode;
|
||||
|
||||
result = true;
|
||||
ex = null;
|
||||
@@ -60,5 +80,18 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private sealed class DummyFunction : Function
|
||||
{
|
||||
protected override Object EvaluateCore(
|
||||
EvaluationContext context,
|
||||
out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Regex s_function = new Regex(@"^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$", RegexOptions.Compiled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
Column = column;
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
internal Int32? FileId { get; set; }
|
||||
[DataMember(Name = "file", EmitDefaultValue = false)]
|
||||
internal Int32? FileId { get; private set; }
|
||||
|
||||
[DataMember(Name = "line", EmitDefaultValue = false)]
|
||||
internal Int32? Line { get; }
|
||||
internal Int32? Line { get; private set; }
|
||||
|
||||
[DataMember(Name = "col", EmitDefaultValue = false)]
|
||||
internal Int32? Column { get; }
|
||||
internal Int32? Column { get; private set; }
|
||||
|
||||
[DataMember(Name = "type", EmitDefaultValue = false)]
|
||||
internal Int32 Type { get; }
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
|
||||
namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
{
|
||||
@@ -106,6 +109,43 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
throw new ArgumentException($"Error while reading '{objectDescription}'. Unexpected value '{literal.ToString()}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Traverses the token and checks whether all required expression values
|
||||
/// and functions are provided.
|
||||
/// </summary>
|
||||
public static bool CheckHasRequiredContext(
|
||||
this TemplateToken token,
|
||||
IReadOnlyObject expressionValues,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var expressionTokens = token.Traverse()
|
||||
.OfType<BasicExpressionToken>()
|
||||
.ToArray();
|
||||
var parser = new ExpressionParser();
|
||||
foreach (var expressionToken in expressionTokens)
|
||||
{
|
||||
var tree = parser.ValidateSyntax(expressionToken.Expression, null);
|
||||
foreach (var node in tree.Traverse())
|
||||
{
|
||||
if (node is NamedValue namedValue)
|
||||
{
|
||||
if (expressionValues?.Keys.Any(x => string.Equals(x, namedValue.Name, StringComparison.OrdinalIgnoreCase)) != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (node is Function function &&
|
||||
!ExpressionConstants.WellKnownFunctions.ContainsKey(function.Name) &&
|
||||
expressionFunctions?.Any(x => string.Equals(x.Name, function.Name, StringComparison.OrdinalIgnoreCase)) != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all tokens (depth first)
|
||||
/// </summary>
|
||||
|
||||
@@ -115,13 +115,12 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
Object value,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
base.WriteJson(writer, value, serializer);
|
||||
if (value is TemplateToken token)
|
||||
{
|
||||
switch (token.Type)
|
||||
{
|
||||
case TokenType.Null:
|
||||
if (token.Line == null && token.Column == null)
|
||||
if (token.FileId == null && token.Line == null && token.Column == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
}
|
||||
@@ -130,12 +129,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue(token.Type);
|
||||
if (token.FileId != null)
|
||||
{
|
||||
writer.WritePropertyName("file");
|
||||
writer.WriteValue(token.FileId);
|
||||
}
|
||||
if (token.Line != null)
|
||||
{
|
||||
writer.WritePropertyName("line");
|
||||
writer.WriteValue(token.Line);
|
||||
}
|
||||
if (token.Line != null)
|
||||
if (token.Column != null)
|
||||
{
|
||||
writer.WritePropertyName("col");
|
||||
writer.WriteValue(token.Column);
|
||||
@@ -146,7 +150,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
|
||||
case TokenType.Boolean:
|
||||
var booleanToken = token as BooleanToken;
|
||||
if (token.Line == null && token.Column == null)
|
||||
if (token.FileId == null && token.Line == null && token.Column == null)
|
||||
{
|
||||
writer.WriteValue(booleanToken.Value);
|
||||
}
|
||||
@@ -155,12 +159,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue(token.Type);
|
||||
if (token.FileId != null)
|
||||
{
|
||||
writer.WritePropertyName("file");
|
||||
writer.WriteValue(token.FileId);
|
||||
}
|
||||
if (token.Line != null)
|
||||
{
|
||||
writer.WritePropertyName("line");
|
||||
writer.WriteValue(token.Line);
|
||||
}
|
||||
if (token.Line != null)
|
||||
if (token.Column != null)
|
||||
{
|
||||
writer.WritePropertyName("col");
|
||||
writer.WriteValue(token.Column);
|
||||
@@ -173,7 +182,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
|
||||
case TokenType.Number:
|
||||
var numberToken = token as NumberToken;
|
||||
if (token.Line == null && token.Column == null)
|
||||
if (token.FileId == null && token.Line == null && token.Column == null)
|
||||
{
|
||||
writer.WriteValue(numberToken.Value);
|
||||
}
|
||||
@@ -182,12 +191,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue(token.Type);
|
||||
if (token.FileId != null)
|
||||
{
|
||||
writer.WritePropertyName("file");
|
||||
writer.WriteValue(token.FileId);
|
||||
}
|
||||
if (token.Line != null)
|
||||
{
|
||||
writer.WritePropertyName("line");
|
||||
writer.WriteValue(token.Line);
|
||||
}
|
||||
if (token.Line != null)
|
||||
if (token.Column != null)
|
||||
{
|
||||
writer.WritePropertyName("col");
|
||||
writer.WriteValue(token.Column);
|
||||
@@ -200,7 +214,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
|
||||
case TokenType.String:
|
||||
var stringToken = token as StringToken;
|
||||
if (token.Line == null && token.Column == null)
|
||||
if (token.FileId == null && token.Line == null && token.Column == null)
|
||||
{
|
||||
writer.WriteValue(stringToken.Value);
|
||||
}
|
||||
@@ -209,12 +223,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue(token.Type);
|
||||
if (token.FileId != null)
|
||||
{
|
||||
writer.WritePropertyName("file");
|
||||
writer.WriteValue(token.FileId);
|
||||
}
|
||||
if (token.Line != null)
|
||||
{
|
||||
writer.WritePropertyName("line");
|
||||
writer.WriteValue(token.Line);
|
||||
}
|
||||
if (token.Line != null)
|
||||
if (token.Column != null)
|
||||
{
|
||||
writer.WritePropertyName("col");
|
||||
writer.WriteValue(token.Column);
|
||||
@@ -230,12 +249,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue(token.Type);
|
||||
if (token.FileId != null)
|
||||
{
|
||||
writer.WritePropertyName("file");
|
||||
writer.WriteValue(token.FileId);
|
||||
}
|
||||
if (token.Line != null)
|
||||
{
|
||||
writer.WritePropertyName("line");
|
||||
writer.WriteValue(token.Line);
|
||||
}
|
||||
if (token.Line != null)
|
||||
if (token.Column != null)
|
||||
{
|
||||
writer.WritePropertyName("col");
|
||||
writer.WriteValue(token.Column);
|
||||
@@ -253,12 +277,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue(token.Type);
|
||||
if (token.FileId != null)
|
||||
{
|
||||
writer.WritePropertyName("file");
|
||||
writer.WriteValue(token.FileId);
|
||||
}
|
||||
if (token.Line != null)
|
||||
{
|
||||
writer.WritePropertyName("line");
|
||||
writer.WriteValue(token.Line);
|
||||
}
|
||||
if (token.Line != null)
|
||||
if (token.Column != null)
|
||||
{
|
||||
writer.WritePropertyName("col");
|
||||
writer.WriteValue(token.Column);
|
||||
@@ -273,12 +302,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue(token.Type);
|
||||
if (token.FileId != null)
|
||||
{
|
||||
writer.WritePropertyName("file");
|
||||
writer.WriteValue(token.FileId);
|
||||
}
|
||||
if (token.Line != null)
|
||||
{
|
||||
writer.WritePropertyName("line");
|
||||
writer.WriteValue(token.Line);
|
||||
}
|
||||
if (token.Line != null)
|
||||
if (token.Column != null)
|
||||
{
|
||||
writer.WritePropertyName("col");
|
||||
writer.WriteValue(token.Column);
|
||||
@@ -301,12 +335,17 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("type");
|
||||
writer.WriteValue(token.Type);
|
||||
if (token.FileId != null)
|
||||
{
|
||||
writer.WritePropertyName("file");
|
||||
writer.WriteValue(token.FileId);
|
||||
}
|
||||
if (token.Line != null)
|
||||
{
|
||||
writer.WritePropertyName("line");
|
||||
writer.WriteValue(token.Line);
|
||||
}
|
||||
if (token.Line != null)
|
||||
if (token.Column != null)
|
||||
{
|
||||
writer.WritePropertyName("col");
|
||||
writer.WriteValue(token.Column);
|
||||
|
||||
@@ -39,7 +39,10 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
DictionaryContextData contextData,
|
||||
WorkspaceOptions workspaceOptions,
|
||||
IEnumerable<JobStep> steps,
|
||||
IEnumerable<ContextScope> scopes)
|
||||
IEnumerable<ContextScope> scopes,
|
||||
IList<String> fileTable,
|
||||
TemplateToken jobOutputs,
|
||||
IList<TemplateToken> defaults)
|
||||
{
|
||||
this.MessageType = JobRequestMessageTypes.PipelineAgentJobRequest;
|
||||
this.Plan = plan;
|
||||
@@ -51,6 +54,7 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
this.Timeline = timeline;
|
||||
this.Resources = jobResources;
|
||||
this.Workspace = workspaceOptions;
|
||||
this.JobOutputs = jobOutputs;
|
||||
|
||||
m_variables = new Dictionary<String, VariableValue>(variables, StringComparer.OrdinalIgnoreCase);
|
||||
m_maskHints = new List<MaskHint>(maskHints);
|
||||
@@ -66,6 +70,11 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
m_environmentVariables = new List<TemplateToken>(environmentVariables);
|
||||
}
|
||||
|
||||
if (defaults?.Count > 0)
|
||||
{
|
||||
m_defaults = new List<TemplateToken>(defaults);
|
||||
}
|
||||
|
||||
this.ContextData = new Dictionary<String, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||
if (contextData?.Count > 0)
|
||||
{
|
||||
@@ -74,6 +83,11 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
this.ContextData[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileTable?.Count > 0)
|
||||
{
|
||||
m_fileTable = new List<String>(fileTable);
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
@@ -132,6 +146,13 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken JobOutputs
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public Int64 RequestId
|
||||
{
|
||||
@@ -198,6 +219,21 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hierarchy of defaults to overlay, last wins.
|
||||
/// </summary>
|
||||
public IList<TemplateToken> Defaults
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_defaults == null)
|
||||
{
|
||||
m_defaults = new List<TemplateToken>();
|
||||
}
|
||||
return m_defaults;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of variables associated with the current context.
|
||||
/// </summary>
|
||||
@@ -237,6 +273,21 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the table of files used when parsing the pipeline (e.g. yaml files)
|
||||
/// </summary>
|
||||
public IList<String> FileTable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_fileTable == null)
|
||||
{
|
||||
m_fileTable = new List<String>();
|
||||
}
|
||||
return m_fileTable;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||
public void SetJobSidecarContainers(IDictionary<String, String> value)
|
||||
{
|
||||
@@ -345,6 +396,16 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
m_environmentVariables = null;
|
||||
}
|
||||
|
||||
if (m_defaults?.Count == 0)
|
||||
{
|
||||
m_defaults = null;
|
||||
}
|
||||
|
||||
if (m_fileTable?.Count == 0)
|
||||
{
|
||||
m_fileTable = null;
|
||||
}
|
||||
|
||||
if (m_maskHints?.Count == 0)
|
||||
{
|
||||
m_maskHints = null;
|
||||
@@ -374,6 +435,12 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
[DataMember(Name = "EnvironmentVariables", EmitDefaultValue = false)]
|
||||
private List<TemplateToken> m_environmentVariables;
|
||||
|
||||
[DataMember(Name = "Defaults", EmitDefaultValue = false)]
|
||||
private List<TemplateToken> m_defaults;
|
||||
|
||||
[DataMember(Name = "FileTable", EmitDefaultValue = false)]
|
||||
private List<String> m_fileTable;
|
||||
|
||||
[DataMember(Name = "Mask", EmitDefaultValue = false)]
|
||||
private List<MaskHint> m_maskHints;
|
||||
|
||||
|
||||
@@ -35,6 +35,19 @@ namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(DictionaryContextData)}' was expected.");
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static CaseSensitiveDictionaryContextData AssertCaseSensitiveDictionary(
|
||||
this PipelineContextData value,
|
||||
String objectDescription)
|
||||
{
|
||||
if (value is CaseSensitiveDictionaryContextData dictionary)
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(CaseSensitiveDictionaryContextData)}' was expected.");
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static StringContextData AssertString(
|
||||
this PipelineContextData value,
|
||||
|
||||
@@ -8,12 +8,14 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
{
|
||||
public const String Always = "always";
|
||||
public const String BooleanStepsContext = "boolean-steps-context";
|
||||
public const String BooleanStrategyContext = "boolean-strategy-context";
|
||||
public const String CancelTimeoutMinutes = "cancel-timeout-minutes";
|
||||
public const String Cancelled = "cancelled";
|
||||
public const String Checkout = "checkout";
|
||||
public const String Clean = "clean";
|
||||
public const String Container = "container";
|
||||
public const String ContinueOnError = "continue-on-error";
|
||||
public const String Defaults = "defaults";
|
||||
public const String Env = "env";
|
||||
public const String Event = "event";
|
||||
public const String EventPattern = "github.event";
|
||||
@@ -23,13 +25,18 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
public const String FetchDepth = "fetch-depth";
|
||||
public const String GeneratedId = "generated-id";
|
||||
public const String GitHub = "github";
|
||||
public const String HashFiles = "hashFiles";
|
||||
public const String Id = "id";
|
||||
public const String If = "if";
|
||||
public const String Image = "image";
|
||||
public const String Include = "include";
|
||||
public const String Inputs = "inputs";
|
||||
public const String Job = "job";
|
||||
public const String JobDefaultsRun = "job-defaults-run";
|
||||
public const String JobIfResult = "job-if-result";
|
||||
public const String JobOutputs = "job-outputs";
|
||||
public const String Jobs = "jobs";
|
||||
public const String Labels = "labels";
|
||||
public const String Lfs = "lfs";
|
||||
public const String Matrix = "matrix";
|
||||
public const String MaxParallel = "max-parallel";
|
||||
@@ -56,6 +63,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
public const String Shell = "shell";
|
||||
public const String Skipped = "skipped";
|
||||
public const String StepEnv = "step-env";
|
||||
public const String StepIfResult = "step-if-result";
|
||||
public const String Steps = "steps";
|
||||
public const String StepsScopeInputs = "steps-scope-inputs";
|
||||
public const String StepsScopeOutputs = "steps-scope-outputs";
|
||||
|
||||
@@ -16,6 +16,20 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
{
|
||||
internal static class PipelineTemplateConverter
|
||||
{
|
||||
internal static Boolean ConvertToIfResult(
|
||||
TemplateContext context,
|
||||
TemplateToken ifResult)
|
||||
{
|
||||
var expression = ifResult.Traverse().FirstOrDefault(x => x is ExpressionToken);
|
||||
if (expression != null)
|
||||
{
|
||||
throw new ArgumentException($"Unexpected type '{expression.GetType().Name}' encountered while reading 'if'.");
|
||||
}
|
||||
|
||||
var evaluationResult = EvaluationResult.CreateIntermediateResult(null, ifResult);
|
||||
return evaluationResult.IsTruthy;
|
||||
}
|
||||
|
||||
internal static Boolean? ConvertToStepContinueOnError(
|
||||
TemplateContext context,
|
||||
TemplateToken token,
|
||||
@@ -154,6 +168,11 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
|
||||
if (value is StringToken containerLiteral)
|
||||
{
|
||||
if (String.IsNullOrEmpty(containerLiteral.Value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
result.Image = containerLiteral.Value;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk.Functions;
|
||||
using GitHub.DistributedTask.ObjectTemplating;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Schema;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
@@ -14,12 +14,16 @@ using ITraceWriter = GitHub.DistributedTask.ObjectTemplating.ITraceWriter;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates parts of the workflow DOM. For example, a job strategy or step inputs.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class PipelineTemplateEvaluator
|
||||
{
|
||||
public PipelineTemplateEvaluator(
|
||||
ITraceWriter trace,
|
||||
TemplateSchema schema)
|
||||
TemplateSchema schema,
|
||||
IList<String> fileTable)
|
||||
{
|
||||
if (!String.Equals(schema.Version, PipelineTemplateConstants.Workflow_1_0, StringComparison.Ordinal))
|
||||
{
|
||||
@@ -28,6 +32,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
|
||||
m_trace = trace;
|
||||
m_schema = schema;
|
||||
m_fileTable = fileTable;
|
||||
}
|
||||
|
||||
public Int32 MaxDepth => 50;
|
||||
@@ -48,13 +53,14 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
|
||||
public DictionaryContextData EvaluateStepScopeInputs(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData)
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(DictionaryContextData);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData);
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.StepsScopeInputs, token, 0, null, omitHeader: true);
|
||||
@@ -74,13 +80,14 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
|
||||
public DictionaryContextData EvaluateStepScopeOutputs(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData)
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(DictionaryContextData);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData);
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.StepsScopeOutputs, token, 0, null, omitHeader: true);
|
||||
@@ -100,13 +107,14 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
|
||||
public Boolean EvaluateStepContinueOnError(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData)
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(Boolean?);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData);
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.BooleanStepsContext, token, 0, null, omitHeader: true);
|
||||
@@ -124,16 +132,44 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
public String EvaluateStepDisplayName(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(String);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.StringStepsContext, token, 0, null, omitHeader: true);
|
||||
context.Errors.Check();
|
||||
result = PipelineTemplateConverter.ConvertToStepDisplayName(context, token);
|
||||
}
|
||||
catch (Exception ex) when (!(ex is TemplateValidationException))
|
||||
{
|
||||
context.Errors.Add(ex);
|
||||
}
|
||||
|
||||
context.Errors.Check();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Dictionary<String, String> EvaluateStepEnvironment(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions,
|
||||
StringComparer keyComparer)
|
||||
{
|
||||
var result = default(Dictionary<String, String>);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData);
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.StepEnv, token, 0, null, omitHeader: true);
|
||||
@@ -151,15 +187,44 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
return result ?? new Dictionary<String, String>(keyComparer);
|
||||
}
|
||||
|
||||
public Boolean EvaluateStepIf(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions,
|
||||
IEnumerable<KeyValuePair<String, Object>> expressionState)
|
||||
{
|
||||
var result = default(Boolean?);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData, expressionFunctions, expressionState);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.StepIfResult, token, 0, null, omitHeader: true);
|
||||
context.Errors.Check();
|
||||
result = PipelineTemplateConverter.ConvertToIfResult(context, token);
|
||||
}
|
||||
catch (Exception ex) when (!(ex is TemplateValidationException))
|
||||
{
|
||||
context.Errors.Add(ex);
|
||||
}
|
||||
|
||||
context.Errors.Check();
|
||||
}
|
||||
|
||||
return result ?? throw new InvalidOperationException("Step if cannot be null");
|
||||
}
|
||||
|
||||
public Dictionary<String, String> EvaluateStepInputs(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData)
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(Dictionary<String, String>);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData);
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.StepWith, token, 0, null, omitHeader: true);
|
||||
@@ -179,13 +244,14 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
|
||||
public Int32 EvaluateStepTimeout(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData)
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(Int32?);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData);
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.NumberStepsContext, token, 0, null, omitHeader: true);
|
||||
@@ -205,13 +271,14 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
|
||||
public JobContainer EvaluateJobContainer(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData)
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(JobContainer);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData);
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Container, token, 0, null, omitHeader: true);
|
||||
@@ -229,15 +296,90 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
return result;
|
||||
}
|
||||
|
||||
public Dictionary<String, String> EvaluateJobOutput(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(Dictionary<String, String>);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.JobOutputs, token, 0, null, omitHeader: true);
|
||||
context.Errors.Check();
|
||||
result = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
|
||||
var mapping = token.AssertMapping("outputs");
|
||||
foreach (var pair in mapping)
|
||||
{
|
||||
// Literal key
|
||||
var key = pair.Key.AssertString("output key");
|
||||
|
||||
// Literal value
|
||||
var value = pair.Value.AssertString("output value");
|
||||
result[key.Value] = value.Value;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!(ex is TemplateValidationException))
|
||||
{
|
||||
context.Errors.Add(ex);
|
||||
}
|
||||
|
||||
context.Errors.Check();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Dictionary<String, String> EvaluateJobDefaultsRun(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(Dictionary<String, String>);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.JobDefaultsRun, token, 0, null, omitHeader: true);
|
||||
context.Errors.Check();
|
||||
result = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
|
||||
var mapping = token.AssertMapping("defaults run");
|
||||
foreach (var pair in mapping)
|
||||
{
|
||||
// Literal key
|
||||
var key = pair.Key.AssertString("defaults run key");
|
||||
|
||||
// Literal value
|
||||
var value = pair.Value.AssertString("defaults run value");
|
||||
result[key.Value] = value.Value;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!(ex is TemplateValidationException))
|
||||
{
|
||||
context.Errors.Add(ex);
|
||||
}
|
||||
|
||||
context.Errors.Check();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IList<KeyValuePair<String, JobContainer>> EvaluateJobServiceContainers(
|
||||
TemplateToken token,
|
||||
DictionaryContextData contextData)
|
||||
DictionaryContextData contextData,
|
||||
IList<IFunctionInfo> expressionFunctions)
|
||||
{
|
||||
var result = default(List<KeyValuePair<String, JobContainer>>);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
var context = CreateContext(contextData);
|
||||
var context = CreateContext(contextData, expressionFunctions);
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Services, token, 0, null, omitHeader: true);
|
||||
@@ -255,62 +397,10 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
return result;
|
||||
}
|
||||
|
||||
public Boolean TryEvaluateStepDisplayName(
|
||||
TemplateToken token,
|
||||
private TemplateContext CreateContext(
|
||||
DictionaryContextData contextData,
|
||||
out String stepName)
|
||||
{
|
||||
stepName = default(String);
|
||||
var context = CreateContext(contextData);
|
||||
|
||||
if (token != null && token.Type != TokenType.Null)
|
||||
{
|
||||
// We should only evaluate basic expressions if we are sure we have context on all the Named Values and functions
|
||||
// Otherwise return and use a default name
|
||||
if (token is BasicExpressionToken expressionToken)
|
||||
{
|
||||
ExpressionNode root = null;
|
||||
try
|
||||
{
|
||||
root = new ExpressionParser().ValidateSyntax(expressionToken.Expression, null) as ExpressionNode;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
context.Errors.Add(exception);
|
||||
context.Errors.Check();
|
||||
}
|
||||
foreach (var node in root.Traverse())
|
||||
{
|
||||
if (node is NamedValue namedValue && !contextData.ContainsKey(namedValue.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (node is Function function &&
|
||||
!context.ExpressionFunctions.Any(item => String.Equals(item.Name, function.Name)) &&
|
||||
!ExpressionConstants.WellKnownFunctions.ContainsKey(function.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.StringStepsContext, token, 0, null, omitHeader: true);
|
||||
context.Errors.Check();
|
||||
stepName = PipelineTemplateConverter.ConvertToStepDisplayName(context, token);
|
||||
}
|
||||
catch (Exception ex) when (!(ex is TemplateValidationException))
|
||||
{
|
||||
context.Errors.Add(ex);
|
||||
}
|
||||
|
||||
context.Errors.Check();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private TemplateContext CreateContext(DictionaryContextData contextData)
|
||||
IList<IFunctionInfo> expressionFunctions,
|
||||
IEnumerable<KeyValuePair<String, Object>> expressionState = null)
|
||||
{
|
||||
var result = new TemplateContext
|
||||
{
|
||||
@@ -324,6 +414,16 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
TraceWriter = m_trace,
|
||||
};
|
||||
|
||||
// Add the file table
|
||||
if (m_fileTable?.Count > 0)
|
||||
{
|
||||
foreach (var file in m_fileTable)
|
||||
{
|
||||
result.GetFileId(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Add named values
|
||||
if (contextData != null)
|
||||
{
|
||||
foreach (var pair in contextData)
|
||||
@@ -332,25 +432,60 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
}
|
||||
}
|
||||
|
||||
// Compat for new agent against old server
|
||||
foreach (var name in s_contextNames)
|
||||
// Add functions
|
||||
var functionNames = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
if (expressionFunctions?.Count > 0)
|
||||
{
|
||||
foreach (var function in expressionFunctions)
|
||||
{
|
||||
result.ExpressionFunctions.Add(function);
|
||||
functionNames.Add(function.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing expression values and expression functions.
|
||||
// This solves the following problems:
|
||||
// - Compat for new agent against old server (new contexts not sent down in job message)
|
||||
// - Evaluating early when all referenced contexts are available, even though all allowed
|
||||
// contexts may not yet be available. For example, evaluating step display name can often
|
||||
// be performed early.
|
||||
foreach (var name in s_expressionValueNames)
|
||||
{
|
||||
if (!result.ExpressionValues.ContainsKey(name))
|
||||
{
|
||||
result.ExpressionValues[name] = null;
|
||||
}
|
||||
}
|
||||
foreach (var name in s_expressionFunctionNames)
|
||||
{
|
||||
if (!functionNames.Contains(name))
|
||||
{
|
||||
result.ExpressionFunctions.Add(new FunctionInfo<NoOperation>(name, 0, Int32.MaxValue));
|
||||
}
|
||||
}
|
||||
|
||||
// Add state
|
||||
if (expressionState != null)
|
||||
{
|
||||
foreach (var pair in expressionState)
|
||||
{
|
||||
result.State[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private readonly ITraceWriter m_trace;
|
||||
private readonly TemplateSchema m_schema;
|
||||
private readonly String[] s_contextNames = new[]
|
||||
private readonly IList<String> m_fileTable;
|
||||
private readonly String[] s_expressionValueNames = new[]
|
||||
{
|
||||
PipelineTemplateConstants.GitHub,
|
||||
PipelineTemplateConstants.Needs,
|
||||
PipelineTemplateConstants.Strategy,
|
||||
PipelineTemplateConstants.Matrix,
|
||||
PipelineTemplateConstants.Needs,
|
||||
PipelineTemplateConstants.Secrets,
|
||||
PipelineTemplateConstants.Steps,
|
||||
PipelineTemplateConstants.Inputs,
|
||||
@@ -358,5 +493,13 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
PipelineTemplateConstants.Runner,
|
||||
PipelineTemplateConstants.Env,
|
||||
};
|
||||
private readonly String[] s_expressionFunctionNames = new[]
|
||||
{
|
||||
PipelineTemplateConstants.Always,
|
||||
PipelineTemplateConstants.Cancelled,
|
||||
PipelineTemplateConstants.Failure,
|
||||
PipelineTemplateConstants.HashFiles,
|
||||
PipelineTemplateConstants.Success,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Schema;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class PipelineTemplateSchemaFactory
|
||||
public static class PipelineTemplateSchemaFactory
|
||||
{
|
||||
public TemplateSchema CreateSchema()
|
||||
public static TemplateSchema GetSchema()
|
||||
{
|
||||
if (s_schema == null)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var json = default(String);
|
||||
@@ -20,7 +24,13 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
}
|
||||
|
||||
var objectReader = new JsonObjectReader(null, json);
|
||||
return TemplateSchema.Load(objectReader);
|
||||
}
|
||||
var schema = TemplateSchema.Load(objectReader);
|
||||
Interlocked.CompareExchange(ref s_schema, schema, null);
|
||||
}
|
||||
|
||||
return s_schema;
|
||||
}
|
||||
|
||||
private static TemplateSchema s_schema;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,572 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.ObjectTemplating;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a YAML file into a TemplateToken
|
||||
/// </summary>
|
||||
internal sealed class YamlObjectReader : IObjectReader
|
||||
{
|
||||
internal YamlObjectReader(
|
||||
Int32? fileId,
|
||||
TextReader input)
|
||||
{
|
||||
m_fileId = fileId;
|
||||
m_parser = new Parser(input);
|
||||
}
|
||||
|
||||
public Boolean AllowLiteral(out LiteralToken value)
|
||||
{
|
||||
if (EvaluateCurrent() is Scalar scalar)
|
||||
{
|
||||
// Tag specified
|
||||
if (!String.IsNullOrEmpty(scalar.Tag))
|
||||
{
|
||||
// String tag
|
||||
if (String.Equals(scalar.Tag, c_stringTag, StringComparison.Ordinal))
|
||||
{
|
||||
value = new StringToken(m_fileId, scalar.Start.Line, scalar.Start.Column, scalar.Value);
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not plain style
|
||||
if (scalar.Style != ScalarStyle.Plain)
|
||||
{
|
||||
throw new NotSupportedException($"The scalar style '{scalar.Style}' on line {scalar.Start.Line} and column {scalar.Start.Column} is not valid with the tag '{scalar.Tag}'");
|
||||
}
|
||||
|
||||
// Boolean, Float, Integer, or Null
|
||||
switch (scalar.Tag)
|
||||
{
|
||||
case c_booleanTag:
|
||||
value = ParseBoolean(scalar);
|
||||
break;
|
||||
case c_floatTag:
|
||||
value = ParseFloat(scalar);
|
||||
break;
|
||||
case c_integerTag:
|
||||
value = ParseInteger(scalar);
|
||||
break;
|
||||
case c_nullTag:
|
||||
value = ParseNull(scalar);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected tag '{scalar.Tag}'");
|
||||
}
|
||||
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Plain style, determine type using YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
if (scalar.Style == ScalarStyle.Plain)
|
||||
{
|
||||
if (MatchNull(scalar, out var nullToken))
|
||||
{
|
||||
value = nullToken;
|
||||
}
|
||||
else if (MatchBoolean(scalar, out var booleanToken))
|
||||
{
|
||||
value = booleanToken;
|
||||
}
|
||||
else if (MatchInteger(scalar, out var numberToken) ||
|
||||
MatchFloat(scalar, out numberToken))
|
||||
{
|
||||
value = numberToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = new StringToken(m_fileId, scalar.Start.Line, scalar.Start.Column, scalar.Value);
|
||||
}
|
||||
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise assume string
|
||||
value = new StringToken(m_fileId, scalar.Start.Line, scalar.Start.Column, scalar.Value);
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Boolean AllowSequenceStart(out SequenceToken value)
|
||||
{
|
||||
if (EvaluateCurrent() is SequenceStart sequenceStart)
|
||||
{
|
||||
value = new SequenceToken(m_fileId, sequenceStart.Start.Line, sequenceStart.Start.Column);
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Boolean AllowSequenceEnd()
|
||||
{
|
||||
if (EvaluateCurrent() is SequenceEnd)
|
||||
{
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Boolean AllowMappingStart(out MappingToken value)
|
||||
{
|
||||
if (EvaluateCurrent() is MappingStart mappingStart)
|
||||
{
|
||||
value = new MappingToken(m_fileId, mappingStart.Start.Line, mappingStart.Start.Column);
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Boolean AllowMappingEnd()
|
||||
{
|
||||
if (EvaluateCurrent() is MappingEnd)
|
||||
{
|
||||
MoveNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumes the last parsing events, which are expected to be DocumentEnd and StreamEnd.
|
||||
/// </summary>
|
||||
public void ValidateEnd()
|
||||
{
|
||||
if (EvaluateCurrent() is DocumentEnd)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Expected document end parse event");
|
||||
}
|
||||
|
||||
if (EvaluateCurrent() is StreamEnd)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Expected stream end parse event");
|
||||
}
|
||||
|
||||
if (MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Expected end of parse events");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumes the first parsing events, which are expected to be StreamStart and DocumentStart.
|
||||
/// </summary>
|
||||
public void ValidateStart()
|
||||
{
|
||||
if (EvaluateCurrent() != null)
|
||||
{
|
||||
throw new InvalidOperationException("Unexpected parser state");
|
||||
}
|
||||
|
||||
if (!MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Expected a parse event");
|
||||
}
|
||||
|
||||
if (EvaluateCurrent() is StreamStart)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Expected stream start parse event");
|
||||
}
|
||||
|
||||
if (EvaluateCurrent() is DocumentStart)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Expected document start parse event");
|
||||
}
|
||||
}
|
||||
|
||||
private ParsingEvent EvaluateCurrent()
|
||||
{
|
||||
if (m_current == null)
|
||||
{
|
||||
m_current = m_parser.Current;
|
||||
if (m_current != null)
|
||||
{
|
||||
if (m_current is Scalar scalar)
|
||||
{
|
||||
// Verify not using achors
|
||||
if (scalar.Anchor != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Anchors are not currently supported. Remove the anchor '{scalar.Anchor}'");
|
||||
}
|
||||
}
|
||||
else if (m_current is MappingStart mappingStart)
|
||||
{
|
||||
// Verify not using achors
|
||||
if (mappingStart.Anchor != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Anchors are not currently supported. Remove the anchor '{mappingStart.Anchor}'");
|
||||
}
|
||||
}
|
||||
else if (m_current is SequenceStart sequenceStart)
|
||||
{
|
||||
// Verify not using achors
|
||||
if (sequenceStart.Anchor != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Anchors are not currently supported. Remove the anchor '{sequenceStart.Anchor}'");
|
||||
}
|
||||
}
|
||||
else if (!(m_current is MappingEnd) &&
|
||||
!(m_current is SequenceEnd) &&
|
||||
!(m_current is DocumentStart) &&
|
||||
!(m_current is DocumentEnd) &&
|
||||
!(m_current is StreamStart) &&
|
||||
!(m_current is StreamEnd))
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected parsing event type: {m_current.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m_current;
|
||||
}
|
||||
|
||||
private Boolean MoveNext()
|
||||
{
|
||||
m_current = null;
|
||||
return m_parser.MoveNext();
|
||||
}
|
||||
|
||||
private BooleanToken ParseBoolean(Scalar scalar)
|
||||
{
|
||||
if (MatchBoolean(scalar, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
ThrowInvalidValue(scalar, c_booleanTag); // throws
|
||||
return default;
|
||||
}
|
||||
|
||||
private NumberToken ParseFloat(Scalar scalar)
|
||||
{
|
||||
if (MatchFloat(scalar, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
ThrowInvalidValue(scalar, c_floatTag); // throws
|
||||
return default;
|
||||
}
|
||||
|
||||
private NumberToken ParseInteger(Scalar scalar)
|
||||
{
|
||||
if (MatchInteger(scalar, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
return default;
|
||||
}
|
||||
|
||||
private NullToken ParseNull(Scalar scalar)
|
||||
{
|
||||
if (MatchNull(scalar, out var token))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
|
||||
ThrowInvalidValue(scalar, c_nullTag); // throws
|
||||
return default;
|
||||
}
|
||||
|
||||
private Boolean MatchBoolean(
|
||||
Scalar scalar,
|
||||
out BooleanToken value)
|
||||
{
|
||||
// YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
switch (scalar.Value ?? String.Empty)
|
||||
{
|
||||
case "true":
|
||||
case "True":
|
||||
case "TRUE":
|
||||
value = new BooleanToken(m_fileId, scalar.Start.Line, scalar.Start.Column, true);
|
||||
return true;
|
||||
case "false":
|
||||
case "False":
|
||||
case "FALSE":
|
||||
value = new BooleanToken(m_fileId, scalar.Start.Line, scalar.Start.Column, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private Boolean MatchFloat(
|
||||
Scalar scalar,
|
||||
out NumberToken value)
|
||||
{
|
||||
// YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
var str = scalar.Value;
|
||||
if (!String.IsNullOrEmpty(str))
|
||||
{
|
||||
// Check for [-+]?(\.inf|\.Inf|\.INF)|\.nan|\.NaN|\.NAN
|
||||
switch (str)
|
||||
{
|
||||
case ".inf":
|
||||
case ".Inf":
|
||||
case ".INF":
|
||||
case "+.inf":
|
||||
case "+.Inf":
|
||||
case "+.INF":
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, Double.PositiveInfinity);
|
||||
return true;
|
||||
case "-.inf":
|
||||
case "-.Inf":
|
||||
case "-.INF":
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, Double.NegativeInfinity);
|
||||
return true;
|
||||
case ".nan":
|
||||
case ".NaN":
|
||||
case ".NAN":
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, Double.NaN);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise check [-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?
|
||||
|
||||
// Skip leading sign
|
||||
var index = str[0] == '-' || str[0] == '+' ? 1 : 0;
|
||||
|
||||
// Check for integer portion
|
||||
var length = str.Length;
|
||||
var hasInteger = false;
|
||||
while (index < length && str[index] >= '0' && str[index] <= '9')
|
||||
{
|
||||
hasInteger = true;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check for decimal point
|
||||
var hasDot = false;
|
||||
if (index < length && str[index] == '.')
|
||||
{
|
||||
hasDot = true;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check for decimal portion
|
||||
var hasDecimal = false;
|
||||
while (index < length && str[index] >= '0' && str[index] <= '9')
|
||||
{
|
||||
hasDecimal = true;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check [-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)
|
||||
if ((hasDot && hasDecimal) || hasInteger)
|
||||
{
|
||||
// Check for end
|
||||
if (index == length)
|
||||
{
|
||||
// Try parse
|
||||
if (Double.TryParse(str, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, doubleValue);
|
||||
return true;
|
||||
}
|
||||
// Otherwise exceeds range
|
||||
else
|
||||
{
|
||||
ThrowInvalidValue(scalar, c_floatTag); // throws
|
||||
}
|
||||
}
|
||||
// Check [eE][-+]?[0-9]
|
||||
else if (index < length && (str[index] == 'e' || str[index] == 'E'))
|
||||
{
|
||||
index++;
|
||||
|
||||
// Skip sign
|
||||
if (index < length && (str[index] == '-' || str[index] == '+'))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check for exponent
|
||||
var hasExponent = false;
|
||||
while (index < length && str[index] >= '0' && str[index] <= '9')
|
||||
{
|
||||
hasExponent = true;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Check for end
|
||||
if (hasExponent && index == length)
|
||||
{
|
||||
// Try parse
|
||||
if (Double.TryParse(str, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, (Double)doubleValue);
|
||||
return true;
|
||||
}
|
||||
// Otherwise exceeds range
|
||||
else
|
||||
{
|
||||
ThrowInvalidValue(scalar, c_floatTag); // throws
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private Boolean MatchInteger(
|
||||
Scalar scalar,
|
||||
out NumberToken value)
|
||||
{
|
||||
// YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
var str = scalar.Value;
|
||||
if (!String.IsNullOrEmpty(str))
|
||||
{
|
||||
// Check for [0-9]+
|
||||
var firstChar = str[0];
|
||||
if (firstChar >= '0' && firstChar <= '9' &&
|
||||
str.Skip(1).All(x => x >= '0' && x <= '9'))
|
||||
{
|
||||
// Try parse
|
||||
if (Double.TryParse(str, NumberStyles.None, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, doubleValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise exceeds range
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
}
|
||||
// Check for (-|+)[0-9]+
|
||||
else if ((firstChar == '-' || firstChar == '+') &&
|
||||
str.Length > 1 &&
|
||||
str.Skip(1).All(x => x >= '0' && x <= '9'))
|
||||
{
|
||||
// Try parse
|
||||
if (Double.TryParse(str, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, doubleValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise exceeds range
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
}
|
||||
// Check for 0x[0-9a-fA-F]+
|
||||
else if (firstChar == '0' &&
|
||||
str.Length > 2 &&
|
||||
str[1] == 'x' &&
|
||||
str.Skip(2).All(x => (x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F')))
|
||||
{
|
||||
// Try parse
|
||||
if (Int32.TryParse(str.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var integerValue))
|
||||
{
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, integerValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise exceeds range
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
}
|
||||
// Check for 0o[0-9]+
|
||||
else if (firstChar == '0' &&
|
||||
str.Length > 2 &&
|
||||
str[1] == 'o' &&
|
||||
str.Skip(2).All(x => x >= '0' && x <= '7'))
|
||||
{
|
||||
// Try parse
|
||||
var integerValue = default(Int32);
|
||||
try
|
||||
{
|
||||
integerValue = Convert.ToInt32(str.Substring(2), 8);
|
||||
}
|
||||
// Otherwise exceeds range
|
||||
catch (Exception)
|
||||
{
|
||||
ThrowInvalidValue(scalar, c_integerTag); // throws
|
||||
}
|
||||
|
||||
value = new NumberToken(m_fileId, scalar.Start.Line, scalar.Start.Column, integerValue);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private Boolean MatchNull(
|
||||
Scalar scalar,
|
||||
out NullToken value)
|
||||
{
|
||||
// YAML 1.2 "core" schema https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
switch (scalar.Value ?? String.Empty)
|
||||
{
|
||||
case "":
|
||||
case "null":
|
||||
case "Null":
|
||||
case "NULL":
|
||||
case "~":
|
||||
value = new NullToken(m_fileId, scalar.Start.Line, scalar.Start.Column);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ThrowInvalidValue(
|
||||
Scalar scalar,
|
||||
String tag)
|
||||
{
|
||||
throw new NotSupportedException($"The value '{scalar.Value}' on line {scalar.Start.Line} and column {scalar.Start.Column} is invalid for the type '{scalar.Tag}'");
|
||||
}
|
||||
|
||||
private const String c_booleanTag = "tag:yaml.org,2002:bool";
|
||||
private const String c_floatTag = "tag:yaml.org,2002:float";
|
||||
private const String c_integerTag = "tag:yaml.org,2002:int";
|
||||
private const String c_nullTag = "tag:yaml.org,2002:null";
|
||||
private const String c_stringTag = "tag:yaml.org,2002:string";
|
||||
private readonly Int32? m_fileId;
|
||||
private readonly Parser m_parser;
|
||||
private ParsingEvent m_current;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"properties": {
|
||||
"on": "any",
|
||||
"name": "string",
|
||||
"defaults": "workflow-defaults",
|
||||
"env": "workflow-env",
|
||||
"jobs": "jobs"
|
||||
}
|
||||
@@ -37,6 +38,7 @@
|
||||
"steps-scope-input-value": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -64,6 +66,7 @@
|
||||
"steps-scope-output-value": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -88,6 +91,7 @@
|
||||
"description": "Default input values for a steps template",
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
],
|
||||
@@ -110,6 +114,7 @@
|
||||
"description": "Output values for a steps template",
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -121,6 +126,23 @@
|
||||
"string": {}
|
||||
},
|
||||
|
||||
"workflow-defaults": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"run": "workflow-defaults-run"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"workflow-defaults-run": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"shell": "non-empty-string",
|
||||
"working-directory": "non-empty-string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"workflow-env": {
|
||||
"context": [
|
||||
"github",
|
||||
@@ -143,16 +165,21 @@
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"needs": "needs",
|
||||
"if": "string",
|
||||
"if": "job-if",
|
||||
"strategy": "strategy",
|
||||
"name": "string-strategy-context",
|
||||
"runs-on": "runs-on",
|
||||
"runs-on": {
|
||||
"type": "runs-on",
|
||||
"required": true
|
||||
},
|
||||
"timeout-minutes": "number-strategy-context",
|
||||
"cancel-timeout-minutes": "number-strategy-context",
|
||||
"continue-on-error": "boolean",
|
||||
"continue-on-error": "boolean-strategy-context",
|
||||
"container": "container",
|
||||
"services": "services",
|
||||
"env": "job-env",
|
||||
"outputs": "job-outputs",
|
||||
"defaults": "job-defaults",
|
||||
"steps": "steps"
|
||||
}
|
||||
}
|
||||
@@ -165,9 +192,41 @@
|
||||
]
|
||||
},
|
||||
|
||||
"job-if": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"always(0,0)",
|
||||
"failure(0,MAX)",
|
||||
"cancelled(0,0)",
|
||||
"success(0,MAX)"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
|
||||
"job-if-result": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"always(0,0)",
|
||||
"failure(0,MAX)",
|
||||
"cancelled(0,0)",
|
||||
"success(0,MAX)"
|
||||
],
|
||||
"one-of": [
|
||||
"null",
|
||||
"boolean",
|
||||
"number",
|
||||
"string",
|
||||
"sequence",
|
||||
"mapping"
|
||||
]
|
||||
},
|
||||
|
||||
"strategy": {
|
||||
"context": [
|
||||
"github"
|
||||
"github",
|
||||
"needs"
|
||||
],
|
||||
"mapping": {
|
||||
"properties": {
|
||||
@@ -232,25 +291,24 @@
|
||||
"runs-on": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
],
|
||||
"one-of": [
|
||||
"runs-on-string",
|
||||
"non-empty-string",
|
||||
"sequence-of-non-empty-string",
|
||||
"runs-on-mapping"
|
||||
]
|
||||
},
|
||||
|
||||
"runs-on-string": {
|
||||
"string": {
|
||||
"require-non-empty": true
|
||||
}
|
||||
},
|
||||
|
||||
"runs-on-mapping": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"pool": "non-empty-string"
|
||||
"pool": {
|
||||
"type": "non-empty-string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -258,9 +316,10 @@
|
||||
"job-env": {
|
||||
"context": [
|
||||
"github",
|
||||
"secrets",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
"matrix",
|
||||
"secrets"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
@@ -268,6 +327,37 @@
|
||||
}
|
||||
},
|
||||
|
||||
"job-defaults": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"run": "job-defaults-run"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"job-defaults-run": {
|
||||
"context": [
|
||||
"github",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"needs",
|
||||
"env"
|
||||
],
|
||||
"mapping": {
|
||||
"properties": {
|
||||
"shell": "non-empty-string",
|
||||
"working-directory": "non-empty-string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"job-outputs": {
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "string-runner-context"
|
||||
}
|
||||
},
|
||||
|
||||
"steps": {
|
||||
"sequence": {
|
||||
"item-type": "steps-item"
|
||||
@@ -301,9 +391,12 @@
|
||||
"properties": {
|
||||
"name": "string-steps-context",
|
||||
"id": "non-empty-string",
|
||||
"if": "string",
|
||||
"if": "step-if",
|
||||
"timeout-minutes": "number-steps-context",
|
||||
"run": "string-steps-context",
|
||||
"run": {
|
||||
"type": "string-steps-context",
|
||||
"required": true
|
||||
},
|
||||
"continue-on-error": "boolean-steps-context",
|
||||
"env": "step-env",
|
||||
"working-directory": "string-steps-context",
|
||||
@@ -317,9 +410,12 @@
|
||||
"properties": {
|
||||
"name": "string-steps-context-in-template",
|
||||
"id": "non-empty-string",
|
||||
"if": "string",
|
||||
"if": "step-if-in-template",
|
||||
"timeout-minutes": "number-steps-context-in-template",
|
||||
"run": "string-steps-context-in-template",
|
||||
"run": {
|
||||
"type": "string-steps-context-in-template",
|
||||
"required": true
|
||||
},
|
||||
"continue-on-error": "boolean-steps-context-in-template",
|
||||
"env": "step-env-in-template",
|
||||
"working-directory": "string-steps-context-in-template",
|
||||
@@ -333,10 +429,13 @@
|
||||
"properties": {
|
||||
"name": "string-steps-context",
|
||||
"id": "non-empty-string",
|
||||
"if": "string",
|
||||
"if": "step-if",
|
||||
"continue-on-error": "boolean-steps-context",
|
||||
"timeout-minutes": "number-steps-context",
|
||||
"uses": "non-empty-string",
|
||||
"uses": {
|
||||
"type": "non-empty-string",
|
||||
"required": true
|
||||
},
|
||||
"with": "step-with",
|
||||
"env": "step-env"
|
||||
}
|
||||
@@ -348,16 +447,109 @@
|
||||
"properties": {
|
||||
"name": "string-steps-context-in-template",
|
||||
"id": "non-empty-string",
|
||||
"if": "string",
|
||||
"if": "step-if-in-template",
|
||||
"continue-on-error": "boolean-steps-context-in-template",
|
||||
"timeout-minutes": "number-steps-context-in-template",
|
||||
"uses": "non-empty-string",
|
||||
"uses": {
|
||||
"type": "non-empty-string",
|
||||
"required": true
|
||||
},
|
||||
"with": "step-with-in-template",
|
||||
"env": "step-env-in-template"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"step-if": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"always(0,0)",
|
||||
"failure(0,0)",
|
||||
"cancelled(0,0)",
|
||||
"success(0,0)",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
|
||||
"step-if-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"always(0,0)",
|
||||
"failure(0,0)",
|
||||
"cancelled(0,0)",
|
||||
"success(0,0)",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
|
||||
"step-if-result": {
|
||||
"context": [
|
||||
"github",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"always(0,0)",
|
||||
"failure(0,0)",
|
||||
"cancelled(0,0)",
|
||||
"success(0,0)",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"one-of": [
|
||||
"null",
|
||||
"boolean",
|
||||
"number",
|
||||
"string",
|
||||
"sequence",
|
||||
"mapping"
|
||||
]
|
||||
},
|
||||
|
||||
"step-if-result-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"steps",
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"always(0,0)",
|
||||
"failure(0,0)",
|
||||
"cancelled(0,0)",
|
||||
"success(0,0)",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"one-of": [
|
||||
"null",
|
||||
"boolean",
|
||||
"number",
|
||||
"string",
|
||||
"sequence",
|
||||
"mapping"
|
||||
]
|
||||
},
|
||||
|
||||
"steps-template-reference": {
|
||||
"mapping": {
|
||||
"properties": {
|
||||
@@ -381,6 +573,7 @@
|
||||
"steps-template-reference-inputs": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -398,6 +591,7 @@
|
||||
"steps-template-reference-inputs-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -416,13 +610,15 @@
|
||||
"step-env": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
@@ -433,6 +629,7 @@
|
||||
"step-env-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -440,7 +637,8 @@
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
@@ -451,13 +649,35 @@
|
||||
"step-with": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "string"
|
||||
}
|
||||
},
|
||||
|
||||
"step-with-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
"steps",
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
@@ -468,11 +688,12 @@
|
||||
"container": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
],
|
||||
"one-of": [
|
||||
"non-empty-string",
|
||||
"string",
|
||||
"container-mapping"
|
||||
]
|
||||
},
|
||||
@@ -492,37 +713,33 @@
|
||||
"services": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "container"
|
||||
"loose-value-type": "services-container"
|
||||
}
|
||||
},
|
||||
|
||||
"services-container": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
],
|
||||
"one-of": [
|
||||
"non-empty-string",
|
||||
"container-mapping"
|
||||
]
|
||||
},
|
||||
|
||||
"container-env": {
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "string"
|
||||
}
|
||||
},
|
||||
|
||||
"step-with-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
"steps",
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
],
|
||||
"mapping": {
|
||||
"loose-key-type": "non-empty-string",
|
||||
"loose-value-type": "string"
|
||||
"loose-value-type": "string-runner-context"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -538,9 +755,20 @@
|
||||
}
|
||||
},
|
||||
|
||||
"boolean-strategy-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
],
|
||||
"boolean": {}
|
||||
},
|
||||
|
||||
"number-strategy-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
],
|
||||
@@ -550,6 +778,7 @@
|
||||
"string-strategy-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix"
|
||||
],
|
||||
@@ -559,13 +788,15 @@
|
||||
"boolean-steps-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"boolean": {}
|
||||
},
|
||||
@@ -573,6 +804,7 @@
|
||||
"boolean-steps-context-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -580,7 +812,8 @@
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"boolean": {}
|
||||
},
|
||||
@@ -588,13 +821,15 @@
|
||||
"number-steps-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"number": {}
|
||||
},
|
||||
@@ -602,6 +837,7 @@
|
||||
"number-steps-context-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -609,14 +845,16 @@
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"number": {}
|
||||
},
|
||||
|
||||
"string-steps-context": {
|
||||
"string-runner-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -628,9 +866,26 @@
|
||||
"string": {}
|
||||
},
|
||||
|
||||
"string-steps-context": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
"steps",
|
||||
"job",
|
||||
"runner",
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"string": {}
|
||||
},
|
||||
|
||||
"string-steps-context-in-template": {
|
||||
"context": [
|
||||
"github",
|
||||
"needs",
|
||||
"strategy",
|
||||
"matrix",
|
||||
"secrets",
|
||||
@@ -638,7 +893,8 @@
|
||||
"inputs",
|
||||
"job",
|
||||
"runner",
|
||||
"env"
|
||||
"env",
|
||||
"hashFiles(1,255)"
|
||||
],
|
||||
"string": {}
|
||||
}
|
||||
|
||||
@@ -123,11 +123,12 @@ namespace GitHub.DistributedTask.WebApi
|
||||
Int64 requestId,
|
||||
Guid jobId,
|
||||
TaskResult result,
|
||||
IDictionary<String, VariableValue> outputVariables)
|
||||
Dictionary<String, VariableValue> outputs)
|
||||
: base(JobEventTypes.JobCompleted, jobId)
|
||||
{
|
||||
this.RequestId = requestId;
|
||||
this.Result = result;
|
||||
this.Outputs = outputs;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
@@ -143,6 +144,13 @@ namespace GitHub.DistributedTask.WebApi
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public IDictionary<String, VariableValue> Outputs
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user