mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
80 Commits
v2.286.1
...
users/tihu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d316c0a75 | ||
|
|
1c582abc8b | ||
|
|
44d4d076fe | ||
|
|
b6195624ac | ||
|
|
ead3509d5a | ||
|
|
fee24199cb | ||
|
|
c8cb600ac7 | ||
|
|
f48f314a70 | ||
|
|
7b677e0618 | ||
|
|
d70f9f6174 | ||
|
|
0343e76789 | ||
|
|
909b05eb66 | ||
|
|
2e3976cf97 | ||
|
|
052ac521b0 | ||
|
|
408d6c579c | ||
|
|
46258428cd | ||
|
|
eb9a604b63 | ||
|
|
8792d8e5ee | ||
|
|
87e86e3d72 | ||
|
|
48b6cd9a42 | ||
|
|
d081289ed5 | ||
|
|
7d5e9cd70f | ||
|
|
98aa9c1152 | ||
|
|
ddc700e9eb | ||
|
|
a0458aebfe | ||
|
|
b2c6d093b2 | ||
|
|
292a2e0ab3 | ||
|
|
29cee52276 | ||
|
|
ad0d0c4d0a | ||
|
|
2c6064a655 | ||
|
|
af6c8e6edd | ||
|
|
c15d3f10b2 | ||
|
|
bdf1e90503 | ||
|
|
100c99f263 | ||
|
|
e8ccafea63 | ||
|
|
02d2cb8fcd | ||
|
|
0cbf3351f4 | ||
|
|
6abef8199f | ||
|
|
ec9830836b | ||
|
|
460c32a337 | ||
|
|
934027da60 | ||
|
|
28f0027938 | ||
|
|
17153c9b29 | ||
|
|
a65ac083b4 | ||
|
|
882f36dcf8 | ||
|
|
f2578529b0 | ||
|
|
bd77ccf34e | ||
|
|
cb19da9638 | ||
|
|
d64190927f | ||
|
|
101b74cab6 | ||
|
|
c06da82ccd | ||
|
|
374989b280 | ||
|
|
47fee8cd64 | ||
|
|
85dcd93d98 | ||
|
|
bac91075f4 | ||
|
|
9240a1cf6c | ||
|
|
2946801fb6 | ||
|
|
1a0d588d3a | ||
|
|
192ebfeccf | ||
|
|
f2347b7a59 | ||
|
|
8f160bc084 | ||
|
|
47ba1203c9 | ||
|
|
dc8b1b685f | ||
|
|
8eacbdc79f | ||
|
|
6b4a95cdb1 | ||
|
|
c95d5eae30 | ||
|
|
ea67ff9647 | ||
|
|
d7d38e173e | ||
|
|
ac31fd10b2 | ||
|
|
d8251bf912 | ||
|
|
715bb7cca8 | ||
|
|
47dfebdf48 | ||
|
|
7cb198a554 | ||
|
|
7616e9b7aa | ||
|
|
3b8475de3e | ||
|
|
ba9766d544 | ||
|
|
29da60a538 | ||
|
|
f2e210e5f3 | ||
|
|
fa32fcf2a1 | ||
|
|
46da23edb1 |
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -37,12 +37,12 @@ jobs:
|
|||||||
devScript: ./dev.sh
|
devScript: ./dev.sh
|
||||||
|
|
||||||
- runtime: win-x64
|
- runtime: win-x64
|
||||||
os: windows-latest
|
os: windows-2019
|
||||||
devScript: ./dev
|
devScript: ./dev
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
@@ -50,13 +50,6 @@ jobs:
|
|||||||
${{ matrix.devScript }} layout Release ${{ matrix.runtime }}
|
${{ matrix.devScript }} layout Release ${{ matrix.runtime }}
|
||||||
working-directory: src
|
working-directory: src
|
||||||
|
|
||||||
# Run tests
|
|
||||||
- name: L0
|
|
||||||
run: |
|
|
||||||
${{ matrix.devScript }} test
|
|
||||||
working-directory: src
|
|
||||||
if: matrix.runtime != 'linux-arm64' && matrix.runtime != 'linux-arm'
|
|
||||||
|
|
||||||
# Check runtime/externals hash
|
# Check runtime/externals hash
|
||||||
- name: Compute/Compare runtime and externals Hash
|
- name: Compute/Compare runtime and externals Hash
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -80,6 +73,13 @@ jobs:
|
|||||||
DOTNET_RUNTIME_HASH: ${{hashFiles('**/_layout_trims/runtime/**/*')}}
|
DOTNET_RUNTIME_HASH: ${{hashFiles('**/_layout_trims/runtime/**/*')}}
|
||||||
EXTERNALS_HASH: ${{hashFiles('**/_layout_trims/externals/**/*')}}
|
EXTERNALS_HASH: ${{hashFiles('**/_layout_trims/externals/**/*')}}
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
- name: L0
|
||||||
|
run: |
|
||||||
|
${{ matrix.devScript }} test
|
||||||
|
working-directory: src
|
||||||
|
if: matrix.runtime != 'linux-arm64' && matrix.runtime != 'linux-arm'
|
||||||
|
|
||||||
# Create runner package tar.gz/zip
|
# Create runner package tar.gz/zip
|
||||||
- name: Package Release
|
- name: Package Release
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
|||||||
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -87,12 +87,12 @@ jobs:
|
|||||||
devScript: ./dev.sh
|
devScript: ./dev.sh
|
||||||
|
|
||||||
- runtime: win-x64
|
- runtime: win-x64
|
||||||
os: windows-latest
|
os: windows-2019
|
||||||
devScript: ./dev
|
devScript: ./dev
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
@@ -101,11 +101,11 @@ jobs:
|
|||||||
working-directory: src
|
working-directory: src
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
- name: L0
|
#- name: L0
|
||||||
run: |
|
# run: |
|
||||||
${{ matrix.devScript }} test
|
# ${{ matrix.devScript }} test
|
||||||
working-directory: src
|
# working-directory: src
|
||||||
if: matrix.runtime != 'linux-arm64' && matrix.runtime != 'linux-arm'
|
# if: matrix.runtime != 'linux-arm64' && matrix.runtime != 'linux-arm'
|
||||||
|
|
||||||
# Create runner package tar.gz/zip
|
# Create runner package tar.gz/zip
|
||||||
- name: Package Release
|
- name: Package Release
|
||||||
@@ -260,6 +260,17 @@ jobs:
|
|||||||
console.log(releaseNote)
|
console.log(releaseNote)
|
||||||
core.setOutput('version', runnerVersion);
|
core.setOutput('version', runnerVersion);
|
||||||
core.setOutput('note', releaseNote);
|
core.setOutput('note', releaseNote);
|
||||||
|
|
||||||
|
- name: Validate Packages HASH
|
||||||
|
working-directory: _package
|
||||||
|
run: |
|
||||||
|
ls -l
|
||||||
|
echo "${{needs.build.outputs.win-x64-sha}} actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip" | shasum -a 256 -c
|
||||||
|
echo "${{needs.build.outputs.osx-x64-sha}} actions-runner-osx-x64-${{ steps.releaseNote.outputs.version }}.tar.gz" | shasum -a 256 -c
|
||||||
|
echo "${{needs.build.outputs.linux-x64-sha}} actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.tar.gz" | shasum -a 256 -c
|
||||||
|
echo "${{needs.build.outputs.linux-arm-sha}} actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.tar.gz" | shasum -a 256 -c
|
||||||
|
echo "${{needs.build.outputs.linux-arm64-sha}} actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz" | shasum -a 256 -c
|
||||||
|
|
||||||
# Create GitHub release
|
# Create GitHub release
|
||||||
- uses: actions/create-release@master
|
- uses: actions/create-release@master
|
||||||
id: createRelease
|
id: createRelease
|
||||||
@@ -525,4 +536,4 @@ jobs:
|
|||||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||||
asset_path: ${{ github.workspace }}/linux-arm64-trimmedpackages.json
|
asset_path: ${{ github.workspace }}/linux-arm64-trimmedpackages.json
|
||||||
asset_name: actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}-trimmedpackages.json
|
asset_name: actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}-trimmedpackages.json
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|||||||
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -13,6 +13,7 @@
|
|||||||
"cwd": "${workspaceFolder}/src",
|
"cwd": "${workspaceFolder}/src",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"requireExactSource": false,
|
"requireExactSource": false,
|
||||||
|
"targetArchitecture": "x86_64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Run",
|
"name": "Run",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"cwd": "${workspaceFolder}/src",
|
"cwd": "${workspaceFolder}/src",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"requireExactSource": false,
|
"requireExactSource": false,
|
||||||
|
"targetArchitecture": "x86_64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Configure",
|
"name": "Configure",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"cwd": "${workspaceFolder}/src",
|
"cwd": "${workspaceFolder}/src",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"requireExactSource": false,
|
"requireExactSource": false,
|
||||||
|
"targetArchitecture": "x86_64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug Worker",
|
"name": "Debug Worker",
|
||||||
@@ -45,6 +48,7 @@
|
|||||||
"request": "attach",
|
"request": "attach",
|
||||||
"processName": "Runner.Worker",
|
"processName": "Runner.Worker",
|
||||||
"requireExactSource": false,
|
"requireExactSource": false,
|
||||||
|
"targetArchitecture": "x86_64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Attach Debugger",
|
"name": "Attach Debugger",
|
||||||
@@ -52,6 +56,8 @@
|
|||||||
"request": "attach",
|
"request": "attach",
|
||||||
"processId": "${command:pickProcess}",
|
"processId": "${command:pickProcess}",
|
||||||
"requireExactSource": false,
|
"requireExactSource": false,
|
||||||
|
"targetArchitecture": "x86_64"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
# GitHub Actions Runner
|
# GitHub Actions Runner
|
||||||
|
|
||||||
[](https://github.com/actions/runner/actions)
|
[](https://github.com/actions/runner/actions)
|
||||||
[](https://github.com/actions-canary/actions-runner-e2e/actions/workflows/runner_e2etest.yml)
|
|
||||||
|
|
||||||
The runner is the application that runs a job from a GitHub Actions workflow. It is used by GitHub Actions in the [hosted virtual environments](https://github.com/actions/virtual-environments), or you can [self-host the runner](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners) in your own environment.
|
The runner is the application that runs a job from a GitHub Actions workflow. It is used by GitHub Actions in the [hosted virtual environments](https://github.com/actions/virtual-environments), or you can [self-host the runner](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners) in your own environment.
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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".
|
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.
|
VSCode has an extensible model for solving this type of problem. VSCode allows users to configure which [problems matchers](https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher) 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.
|
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.
|
||||||
|
|
||||||
@@ -18,21 +18,23 @@ The problem-matcher concept fits well with "setup" actions. For example, the `se
|
|||||||
|
|
||||||
### Registration
|
### Registration
|
||||||
|
|
||||||
#### Using `##` command
|
#### Using `::` command
|
||||||
|
|
||||||
`##[add-matcher]path-to-problem-matcher-config.json`
|
`::add-matcher::path-to-problem-matcher-config.json`
|
||||||
|
|
||||||
Using a `##` command allows for flexibility:
|
Using a `::` command allows for flexibility:
|
||||||
- Ad hoc scripts can register problem matchers
|
- Ad hoc scripts can register problem matchers
|
||||||
- Allows problem matchers to be conditionally registered
|
- 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.
|
Note, if a matcher with the same name is registered a second time, it will clobber the first instance.
|
||||||
|
|
||||||
#### Unregister using `##` command
|
Note, at some point the syntax changed from `##` to `::`.
|
||||||
|
|
||||||
|
#### Unregister using `::` command
|
||||||
|
|
||||||
A way out for rare cases where scoping is a problem.
|
A way out for rare cases where scoping is a problem.
|
||||||
|
|
||||||
`##[remove-matcher]owner`
|
`::remove-matcher::owner`
|
||||||
|
|
||||||
For this to be usable, the `owner` needs to be discoverable. Therefore, debug print the owner on registration.
|
For this to be usable, the `owner` needs to be discoverable. Therefore, debug print the owner on registration.
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ message: ; expected
|
|||||||
fromPath: C:\myrepo\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj
|
fromPath: C:\myrepo\myproject\ConsoleApp1\ClassLibrary1\ClassLibrary1.csproj
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally the line will appear red in the web UI (prefix with `##[error]`).
|
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, an error does not imply task failure. Exit codes communicate failure.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ The runner will look for a file `.setup_info` under the runner's root directory,
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
The runner will use `##[group]` and `##[endgroup]` to fold all detail info into an expandable group.
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,35 @@
|
|||||||
Make sure the runner has access to actions service for GitHub.com or GitHub Enterprise Server
|
Make sure the runner has access to actions service for GitHub.com or GitHub Enterprise Server
|
||||||
|
|
||||||
- For GitHub.com
|
- For GitHub.com
|
||||||
- The runner needs to access https://api.github.com for downloading actions.
|
- The runner needs to access `https://api.github.com` for downloading actions.
|
||||||
- The runner needs to access https://vstoken.actions.githubusercontent.com/_apis/.../ for requesting an access token.
|
- The runner needs to access `https://vstoken.actions.githubusercontent.com/_apis/.../` for requesting an access token.
|
||||||
- The runner needs to access https://pipelines.actions.githubusercontent.com/_apis/.../ for receiving workflow jobs.
|
- The runner needs to access `https://pipelines.actions.githubusercontent.com/_apis/.../` for receiving workflow jobs.
|
||||||
|
|
||||||
|
These can by tested by running the following `curl` commands from your self-hosted runner machine:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -v https://api.github.com/api/v3/zen
|
||||||
|
curl -v https://vstoken.actions.githubusercontent.com/_apis/health
|
||||||
|
curl -v https://pipelines.actions.githubusercontent/_apis/health
|
||||||
|
```
|
||||||
|
|
||||||
- For GitHub Enterprise Server
|
- For GitHub Enterprise Server
|
||||||
- The runner needs to access https://myGHES.com/api/v3 for downloading actions.
|
- The runner needs to access `https://[hostname]/api/v3` for downloading actions.
|
||||||
- The runner needs to access https://myGHES.com/_services/vstoken/_apis/.../ for requesting an access token.
|
- The runner needs to access `https://[hostname]/_services/vstoken/_apis/.../` for requesting an access token.
|
||||||
- The runner needs to access https://myGHES.com/_services/pipelines/_apis/.../ for receiving workflow jobs.
|
- The runner needs to access `https://[hostname]/_services/pipelines/_apis/.../` for receiving workflow jobs.
|
||||||
|
|
||||||
|
These can by tested by running the following `curl` commands from your self-hosted runner machine, replacing `[hostname]` with the hostname of your appliance, for instance `github.example.com`:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -v https://[hostname]/api/v3/zen
|
||||||
|
curl -v https://[hostname]/_services/vstoken/_apis/health
|
||||||
|
curl -v https://[hostname]/_services/pipelines/_apis/health
|
||||||
|
```
|
||||||
|
|
||||||
|
A common cause of this these connectivity issues is if your to GitHub Enterprise Server appliance is using [the self-signed certificate that is enabled the first time](https://docs.github.com/en/enterprise-server/admin/configuration/configuring-network-settings/configuring-tls) your appliance is started. As self-signed certificates are not trusted by web browsers and Git clients, these clients (including the GitHub Actions runner) will report certificate warnings.
|
||||||
|
|
||||||
|
We recommend [upload a certificate signed by a trusted authority](https://docs.github.com/en/enterprise-server/admin/configuration/configuring-network-settings/configuring-tls) to GitHub Enterprise Server, or enabling the built-in ][Let's Encrypt support](https://docs.github.com/en/enterprise-server/admin/configuration/configuring-network-settings/configuring-tls).
|
||||||
|
|
||||||
|
|
||||||
## What is checked?
|
## What is checked?
|
||||||
|
|
||||||
@@ -42,4 +64,4 @@ Make sure the runner has access to actions service for GitHub.com or GitHub Ente
|
|||||||
|
|
||||||
## Still not working?
|
## Still not working?
|
||||||
|
|
||||||
Contact GitHub customer service or log an issue at https://github.com/actions/runner if you think it's a runner issue.
|
Contact [GitHub Support](https://support.github.com] if you have further questuons, or log an issue at https://github.com/actions/runner if you think it's a runner issue.
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
Make sure the built-in node.js has access to GitHub.com or GitHub Enterprise Server.
|
Make sure the built-in node.js has access to GitHub.com or GitHub Enterprise Server.
|
||||||
|
|
||||||
The runner carries it's own copy of node.js executable under `<runner_root>/externals/node12/`.
|
The runner carries its own copy of node.js executable under `<runner_root>/externals/node16/`.
|
||||||
|
|
||||||
All javascript base Actions will get executed by the built-in `node` at `<runner_root>/externals/node12/`.
|
All javascript base Actions will get executed by the built-in `node` at `<runner_root>/externals/node16/`.
|
||||||
|
|
||||||
> Not the `node` from `$PATH`
|
> Not the `node` from `$PATH`
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ An ADR is an Architectural Decision Record. This allows consensus on the direct
|
|||||||
|
|
||||||
  Git for Windows and Linux [Install Here](https://git-scm.com/downloads) (needed for dev sh script)
|
  Git for Windows and Linux [Install Here](https://git-scm.com/downloads) (needed for dev sh script)
|
||||||
|
|
||||||
|
 cURL [Install here](https://curl.se/download.html) (needed for external sh script)
|
||||||
|
|
||||||
|
 Visual Studio 2017 or newer [Install here](https://visualstudio.microsoft.com) (needed for dev sh script)
|
||||||
|
|
||||||
## Quickstart: Run a job from a real repository
|
## Quickstart: Run a job from a real repository
|
||||||
|
|
||||||
If you just want to get from building the sourcecode to using it to execute an action, you will need:
|
If you just want to get from building the sourcecode to using it to execute an action, you will need:
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
## Features
|
## Features
|
||||||
|
- Continue-on-error is now possible for the composite action steps (#1763)
|
||||||
- Bump runtime to dotnet 6 (#1471)
|
- Now it's possible to use context evaluation in the `shell` of composite action run steps (#1767)
|
||||||
- Show service container logs on teardown (#1563)
|
|
||||||
|
|
||||||
## Bugs
|
## Bugs
|
||||||
|
- Fix a bug where job would be marked as 'cancelled' after self-hosted runner going offline (#1792)
|
||||||
- Add masks for multiline secrets from ::add-mask:: (#1521)
|
- Translate paths in `github` and `runner` contexts when running on a container (#1762)
|
||||||
- fix Log size and retention settings not work (#1507)
|
- Warn about invalid flags when configuring or running the runner (#1781)
|
||||||
- Refactor SelfUpdater adding L0 tests. (#1564)
|
- Fix a bug where job hooks would use job level working directory (#1809)
|
||||||
- Fix test failure: /bin/sleep on Macos 11 (Monterey) does not accept the suffix s. (#1472)
|
|
||||||
|
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
|
- Allow warnings about actions using Node v12 (#1735)
|
||||||
- Update dependency check for dotnet 6. (#1551)
|
- Better exception handling when runner is configured with invalid Url or token (#1741)
|
||||||
- Produce trimmed down runner packages. (#1556)
|
- Set user agent for websocket requests (#1791)
|
||||||
- Deleted extra background in github-praph.png, which is displayed in README.md (#1432)
|
- Gracefully handle websocket failures (#1789)
|
||||||
|
|
||||||
|
|
||||||
## Windows x64
|
## Windows x64
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ set -e
|
|||||||
|
|
||||||
flags_found=false
|
flags_found=false
|
||||||
|
|
||||||
while getopts 's:g:n:u:l:' opt; do
|
while getopts 's:g:n:r:u:l:' opt; do
|
||||||
flags_found=true
|
flags_found=true
|
||||||
|
|
||||||
case $opt in
|
case $opt in
|
||||||
@@ -26,6 +26,9 @@ while getopts 's:g:n:u:l:' opt; do
|
|||||||
n)
|
n)
|
||||||
runner_name=$OPTARG
|
runner_name=$OPTARG
|
||||||
;;
|
;;
|
||||||
|
r)
|
||||||
|
runner_group=$OPTARG
|
||||||
|
;;
|
||||||
u)
|
u)
|
||||||
svc_user=$OPTARG
|
svc_user=$OPTARG
|
||||||
;;
|
;;
|
||||||
@@ -44,6 +47,7 @@ Usage:
|
|||||||
-s required scope: repo (:owner/:repo) or org (:organization)
|
-s required scope: repo (:owner/:repo) or org (:organization)
|
||||||
-g optional ghe_hostname: the fully qualified domain name of your GitHub Enterprise Server deployment
|
-g optional ghe_hostname: the fully qualified domain name of your GitHub Enterprise Server deployment
|
||||||
-n optional name of the runner, defaults to hostname
|
-n optional name of the runner, defaults to hostname
|
||||||
|
-r optional name of the runner group to add the runner to, defaults to the Default group
|
||||||
-u optional user svc will run as, defaults to current
|
-u optional user svc will run as, defaults to current
|
||||||
-l optional list of labels (split by comma) applied on the runner"
|
-l optional list of labels (split by comma) applied on the runner"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -59,6 +63,7 @@ if ! "$flags_found"; then
|
|||||||
runner_name=${3:-$(hostname)}
|
runner_name=${3:-$(hostname)}
|
||||||
svc_user=${4:-$USER}
|
svc_user=${4:-$USER}
|
||||||
labels=${5}
|
labels=${5}
|
||||||
|
runner_group=${6}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# apply defaults
|
# apply defaults
|
||||||
@@ -164,8 +169,8 @@ fi
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Configuring ${runner_name} @ $runner_url"
|
echo "Configuring ${runner_name} @ $runner_url"
|
||||||
echo "./config.sh --unattended --url $runner_url --token *** --name $runner_name --labels $labels"
|
echo "./config.sh --unattended --url $runner_url --token *** --name $runner_name ${labels:+--labels $labels} ${runner_group:+--runnergroup \"$runner_group\"}"
|
||||||
sudo -E -u ${svc_user} ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name --labels $labels
|
sudo -E -u ${svc_user} ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name ${labels:+--labels $labels} ${runner_group:+--runnergroup "$runner_group"}
|
||||||
|
|
||||||
#---------------------------------------
|
#---------------------------------------
|
||||||
# Configuring as a service
|
# Configuring as a service
|
||||||
|
|||||||
2
src/Misc/contentHash/externals/linux-arm
vendored
2
src/Misc/contentHash/externals/linux-arm
vendored
@@ -1 +1 @@
|
|||||||
6ca4a0e1c50b7079ead05321dcf5835c1c25f23dc632add8c1c4667d416d103e
|
6ed30a2c1ee403a610d63e82bb230b9ba846a9c25cec9e4ea8672fb6ed4e1a51
|
||||||
2
src/Misc/contentHash/externals/linux-arm64
vendored
2
src/Misc/contentHash/externals/linux-arm64
vendored
@@ -1 +1 @@
|
|||||||
b5951dc607d782d9c7571a7224e940eb0975bb23c54ff25c7afdbf959a417081
|
711c30c51ec52c9b7a9a2eb399d6ab2ab5ee1dc72de11879f2f36f919f163d78
|
||||||
2
src/Misc/contentHash/externals/linux-x64
vendored
2
src/Misc/contentHash/externals/linux-x64
vendored
@@ -1 +1 @@
|
|||||||
af819e92011cc9cbca90e8299f9f7651f2cf6bf45b42920f9a4ca22795486147
|
a49479ca4b4988a06c097e8d22c51fd08a11c13f40807366236213d0e008cf6a
|
||||||
2
src/Misc/contentHash/externals/osx-x64
vendored
2
src/Misc/contentHash/externals/osx-x64
vendored
@@ -1 +1 @@
|
|||||||
aa0e6bf4bfaabf48c962ea3b262dca042629ab332005f73d282faec908847036
|
8e97df75230b843462a9b4c578ccec604ee4b4a1066120c85b04374317fa372b
|
||||||
2
src/Misc/contentHash/externals/win-x64
vendored
2
src/Misc/contentHash/externals/win-x64
vendored
@@ -1 +1 @@
|
|||||||
40328cff2b8229f9b578f32739183bd8f6aab481c21dadc052b09f1c7e8e4665
|
f75a671e5a188c76680739689aa75331a2c09d483dce9c80023518c48fd67a18
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["jest", "@typescript-eslint"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"extends": ["plugin:github/es6"],
|
"extends": ["plugin:github/recommended"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 9,
|
"ecmaVersion": 9,
|
||||||
@@ -17,13 +17,16 @@
|
|||||||
"@typescript-eslint/no-require-imports": "error",
|
"@typescript-eslint/no-require-imports": "error",
|
||||||
"@typescript-eslint/array-type": "error",
|
"@typescript-eslint/array-type": "error",
|
||||||
"@typescript-eslint/await-thenable": "error",
|
"@typescript-eslint/await-thenable": "error",
|
||||||
"@typescript-eslint/ban-ts-ignore": "error",
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"selector": "default",
|
||||||
|
"format": ["camelCase"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"camelcase": "off",
|
"camelcase": "off",
|
||||||
"@typescript-eslint/camelcase": "error",
|
|
||||||
"@typescript-eslint/class-name-casing": "error",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
||||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
"@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-array-constructor": "error",
|
||||||
"@typescript-eslint/no-empty-interface": "error",
|
"@typescript-eslint/no-empty-interface": "error",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
@@ -33,7 +36,6 @@
|
|||||||
"@typescript-eslint/no-misused-new": "error",
|
"@typescript-eslint/no-misused-new": "error",
|
||||||
"@typescript-eslint/no-namespace": "error",
|
"@typescript-eslint/no-namespace": "error",
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
"@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-qualifier": "error",
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||||
"@typescript-eslint/no-useless-constructor": "error",
|
"@typescript-eslint/no-useless-constructor": "error",
|
||||||
@@ -41,19 +43,19 @@
|
|||||||
"@typescript-eslint/prefer-for-of": "warn",
|
"@typescript-eslint/prefer-for-of": "warn",
|
||||||
"@typescript-eslint/prefer-function-type": "warn",
|
"@typescript-eslint/prefer-function-type": "warn",
|
||||||
"@typescript-eslint/prefer-includes": "error",
|
"@typescript-eslint/prefer-includes": "error",
|
||||||
"@typescript-eslint/prefer-interface": "error",
|
|
||||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||||
"@typescript-eslint/promise-function-async": "error",
|
"@typescript-eslint/promise-function-async": "error",
|
||||||
"@typescript-eslint/require-array-sort-compare": "error",
|
"@typescript-eslint/require-array-sort-compare": "error",
|
||||||
"@typescript-eslint/restrict-plus-operands": "error",
|
"@typescript-eslint/restrict-plus-operands": "error",
|
||||||
"semi": "off",
|
|
||||||
"@typescript-eslint/semi": ["error", "never"],
|
"@typescript-eslint/semi": ["error", "never"],
|
||||||
"@typescript-eslint/type-annotation-spacing": "error",
|
"@typescript-eslint/type-annotation-spacing": "error",
|
||||||
"@typescript-eslint/unbound-method": "error"
|
"@typescript-eslint/unbound-method": "error",
|
||||||
|
"filenames/match-regex" : "off",
|
||||||
|
"github/no-then" : 1, // warning
|
||||||
|
"semi": "off"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"node": true,
|
"node": true,
|
||||||
"es6": true,
|
"es6": true
|
||||||
"jest/globals": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5175
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
5175
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,10 +25,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^12.7.12",
|
"@types/node": "^12.7.12",
|
||||||
"@typescript-eslint/parser": "^2.8.0",
|
"@typescript-eslint/parser": "^5.15.0",
|
||||||
"@zeit/ncc": "^0.20.5",
|
"@zeit/ncc": "^0.20.5",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^8.11.0",
|
||||||
"eslint-plugin-github": "^2.0.0",
|
"eslint-plugin-github": "^4.3.5",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"typescript": "^3.6.4"
|
"typescript": "^3.6.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as glob from '@actions/glob'
|
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
|
import * as glob from '@actions/glob'
|
||||||
|
import * as path from 'path'
|
||||||
import * as stream from 'stream'
|
import * as stream from 'stream'
|
||||||
import * as util from 'util'
|
import * as util from 'util'
|
||||||
import * as path from 'path'
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
// arg0 -> node
|
// arg0 -> node
|
||||||
@@ -45,7 +45,7 @@ async function run(): Promise<void> {
|
|||||||
result.end()
|
result.end()
|
||||||
|
|
||||||
if (hasMatch) {
|
if (hasMatch) {
|
||||||
console.log(`Find ${count} files to hash.`)
|
console.log(`Found ${count} files to hash.`)
|
||||||
console.error(`__OUTPUT__${result.digest('hex')}__OUTPUT__`)
|
console.error(`__OUTPUT__${result.digest('hex')}__OUTPUT__`)
|
||||||
} else {
|
} else {
|
||||||
console.error(`__OUTPUT____OUTPUT__`)
|
console.error(`__OUTPUT____OUTPUT__`)
|
||||||
@@ -53,3 +53,11 @@ async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
.then(out => {
|
||||||
|
console.log(out)
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ PACKAGERUNTIME=$1
|
|||||||
PRECACHE=$2
|
PRECACHE=$2
|
||||||
|
|
||||||
NODE_URL=https://nodejs.org/dist
|
NODE_URL=https://nodejs.org/dist
|
||||||
NODE12_VERSION="12.13.1"
|
NODE12_VERSION="12.22.7"
|
||||||
NODE16_VERSION="16.13.0"
|
NODE16_VERSION="16.13.0"
|
||||||
|
|
||||||
get_abs_path() {
|
get_abs_path() {
|
||||||
@@ -143,7 +143,7 @@ fi
|
|||||||
# Download the external tools for Linux PACKAGERUNTIMEs.
|
# Download the external tools for Linux PACKAGERUNTIMEs.
|
||||||
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
|
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
|
||||||
acquireExternalTool "$NODE_URL/v${NODE12_VERSION}/node-v${NODE12_VERSION}-linux-x64.tar.gz" node12 fix_nested_dir
|
acquireExternalTool "$NODE_URL/v${NODE12_VERSION}/node-v${NODE12_VERSION}-linux-x64.tar.gz" node12 fix_nested_dir
|
||||||
acquireExternalTool "https://vstsagenttools.blob.core.windows.net/tools/nodejs/${NODE12_VERSION}/alpine/x64/node-${NODE12_VERSION}-alpine-x64.tar.gz" node12_alpine
|
acquireExternalTool "https://vstsagenttools.blob.core.windows.net/tools/nodejs/${NODE12_VERSION}/alpine/x64/node-v${NODE12_VERSION}-alpine-x64.tar.gz" node12_alpine
|
||||||
acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-linux-x64.tar.gz" node16 fix_nested_dir
|
acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-linux-x64.tar.gz" node16 fix_nested_dir
|
||||||
acquireExternalTool "https://vstsagenttools.blob.core.windows.net/tools/nodejs/${NODE16_VERSION}/alpine/x64/node-v${NODE16_VERSION}-alpine-x64.tar.gz" node16_alpine
|
acquireExternalTool "https://vstsagenttools.blob.core.windows.net/tools/nodejs/${NODE16_VERSION}/alpine/x64/node-v${NODE16_VERSION}-alpine-x64.tar.gz" node16_alpine
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,94 +3,135 @@
|
|||||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||||
|
|
||||||
var childProcess = require("child_process");
|
var childProcess = require("child_process");
|
||||||
var path = require("path")
|
var path = require("path");
|
||||||
|
|
||||||
var supported = ['linux', 'darwin']
|
var supported = ["linux", "darwin"];
|
||||||
|
|
||||||
if (supported.indexOf(process.platform) == -1) {
|
if (supported.indexOf(process.platform) == -1) {
|
||||||
console.log('Unsupported platform: ' + process.platform);
|
console.log("Unsupported platform: " + process.platform);
|
||||||
console.log('Supported platforms are: ' + supported.toString());
|
console.log("Supported platforms are: " + supported.toString());
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
var stopping = false;
|
var stopping = false;
|
||||||
var listener = null;
|
var listener = null;
|
||||||
|
|
||||||
var runService = function () {
|
var exitServiceAfterNFailures = Number(
|
||||||
var listenerExePath = path.join(__dirname, '../bin/Runner.Listener');
|
process.env.GITHUB_ACTIONS_SERVICE_EXIT_AFTER_N_FAILURES
|
||||||
var interactive = process.argv[2] === "interactive";
|
);
|
||||||
|
|
||||||
if (!stopping) {
|
if (exitServiceAfterNFailures <= 0) {
|
||||||
try {
|
exitServiceAfterNFailures = NaN;
|
||||||
if (interactive) {
|
|
||||||
console.log('Starting Runner listener interactively');
|
|
||||||
listener = childProcess.spawn(listenerExePath, ['run'], { env: process.env });
|
|
||||||
} else {
|
|
||||||
console.log('Starting Runner listener with startup type: service');
|
|
||||||
listener = childProcess.spawn(listenerExePath, ['run', '--startuptype', 'service'], { env: process.env });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Started listener process, pid: ${listener.pid}`);
|
|
||||||
|
|
||||||
listener.stdout.on('data', (data) => {
|
|
||||||
process.stdout.write(data.toString('utf8'));
|
|
||||||
});
|
|
||||||
|
|
||||||
listener.stderr.on('data', (data) => {
|
|
||||||
process.stdout.write(data.toString('utf8'));
|
|
||||||
});
|
|
||||||
|
|
||||||
listener.on("error", (err) => {
|
|
||||||
console.log(`Runner listener fail to start with error ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
listener.on('close', (code) => {
|
|
||||||
console.log(`Runner listener exited with error code ${code}`);
|
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
console.log('Runner listener exit with 0 return code, stop the service, no retry needed.');
|
|
||||||
stopping = true;
|
|
||||||
} else if (code === 1) {
|
|
||||||
console.log('Runner listener exit with terminated error, stop the service, no retry needed.');
|
|
||||||
stopping = true;
|
|
||||||
} else if (code === 2) {
|
|
||||||
console.log('Runner listener exit with retryable error, re-launch runner in 5 seconds.');
|
|
||||||
} else if (code === 3) {
|
|
||||||
console.log('Runner listener exit because of updating, re-launch runner in 5 seconds.');
|
|
||||||
} else {
|
|
||||||
console.log('Runner listener exit with undefined return code, re-launch runner in 5 seconds.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stopping) {
|
|
||||||
setTimeout(runService, 5000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (ex) {
|
|
||||||
console.log(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var consecutiveFailureCount = 0;
|
||||||
|
|
||||||
|
var gracefulShutdown = function () {
|
||||||
|
console.log("Shutting down runner listener");
|
||||||
|
stopping = true;
|
||||||
|
if (listener) {
|
||||||
|
console.log("Sending SIGINT to runner listener to stop");
|
||||||
|
listener.kill("SIGINT");
|
||||||
|
|
||||||
|
console.log("Sending SIGKILL to runner listener");
|
||||||
|
setTimeout(() => listener.kill("SIGKILL"), 30000).unref();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var runService = function () {
|
||||||
|
var listenerExePath = path.join(__dirname, "../bin/Runner.Listener");
|
||||||
|
var interactive = process.argv[2] === "interactive";
|
||||||
|
|
||||||
|
if (!stopping) {
|
||||||
|
try {
|
||||||
|
if (interactive) {
|
||||||
|
console.log("Starting Runner listener interactively");
|
||||||
|
listener = childProcess.spawn(listenerExePath, ["run"], {
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Starting Runner listener with startup type: service");
|
||||||
|
listener = childProcess.spawn(
|
||||||
|
listenerExePath,
|
||||||
|
["run", "--startuptype", "service"],
|
||||||
|
{ env: process.env }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Started listener process, pid: ${listener.pid}`);
|
||||||
|
|
||||||
|
listener.stdout.on("data", (data) => {
|
||||||
|
if (data.toString("utf8").includes("Listening for Jobs")) {
|
||||||
|
consecutiveFailureCount = 0;
|
||||||
|
}
|
||||||
|
process.stdout.write(data.toString("utf8"));
|
||||||
|
});
|
||||||
|
|
||||||
|
listener.stderr.on("data", (data) => {
|
||||||
|
process.stdout.write(data.toString("utf8"));
|
||||||
|
});
|
||||||
|
|
||||||
|
listener.on("error", (err) => {
|
||||||
|
console.log(`Runner listener fail to start with error ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
listener.on("close", (code) => {
|
||||||
|
console.log(`Runner listener exited with error code ${code}`);
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
console.log(
|
||||||
|
"Runner listener exit with 0 return code, stop the service, no retry needed."
|
||||||
|
);
|
||||||
|
stopping = true;
|
||||||
|
} else if (code === 1) {
|
||||||
|
console.log(
|
||||||
|
"Runner listener exit with terminated error, stop the service, no retry needed."
|
||||||
|
);
|
||||||
|
stopping = true;
|
||||||
|
} else if (code === 2) {
|
||||||
|
console.log(
|
||||||
|
"Runner listener exit with retryable error, re-launch runner in 5 seconds."
|
||||||
|
);
|
||||||
|
consecutiveFailureCount = 0;
|
||||||
|
} else if (code === 3 || code === 4) {
|
||||||
|
console.log(
|
||||||
|
"Runner listener exit because of updating, re-launch runner in 5 seconds."
|
||||||
|
);
|
||||||
|
consecutiveFailureCount = 0;
|
||||||
|
} else {
|
||||||
|
var messagePrefix = "Runner listener exit with undefined return code";
|
||||||
|
consecutiveFailureCount++;
|
||||||
|
if (
|
||||||
|
!isNaN(exitServiceAfterNFailures) &&
|
||||||
|
consecutiveFailureCount >= exitServiceAfterNFailures
|
||||||
|
) {
|
||||||
|
console.error(
|
||||||
|
`${messagePrefix}, exiting service after ${consecutiveFailureCount} consecutive failures`
|
||||||
|
);
|
||||||
|
gracefulShutdown();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log(`${messagePrefix}, re-launch runner in 5 seconds.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stopping) {
|
||||||
|
setTimeout(runService, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
console.log(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
runService();
|
runService();
|
||||||
console.log('Started running service');
|
console.log("Started running service");
|
||||||
|
|
||||||
var gracefulShutdown = function (code) {
|
process.on("SIGINT", () => {
|
||||||
console.log('Shutting down runner listener');
|
gracefulShutdown();
|
||||||
stopping = true;
|
|
||||||
if (listener) {
|
|
||||||
console.log('Sending SIGINT to runner listener to stop');
|
|
||||||
listener.kill('SIGINT');
|
|
||||||
|
|
||||||
console.log('Sending SIGKILL to runner listener');
|
|
||||||
setTimeout(() => listener.kill('SIGKILL'), 30000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
gracefulShutdown(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
process.on("SIGTERM", () => {
|
||||||
gracefulShutdown(0);
|
gracefulShutdown();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ RUNNER_ROOT=`pwd`
|
|||||||
|
|
||||||
LAUNCH_PATH="${HOME}/Library/LaunchAgents"
|
LAUNCH_PATH="${HOME}/Library/LaunchAgents"
|
||||||
PLIST_PATH="${LAUNCH_PATH}/${SVC_NAME}.plist"
|
PLIST_PATH="${LAUNCH_PATH}/${SVC_NAME}.plist"
|
||||||
TEMPLATE_PATH=./bin/actions.runner.plist.template
|
TEMPLATE_PATH=$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE
|
||||||
|
IS_CUSTOM_TEMPLATE=0
|
||||||
|
if [[ -z $TEMPLATE_PATH ]]; then
|
||||||
|
TEMPLATE_PATH=./bin/actions.runner.plist.template
|
||||||
|
else
|
||||||
|
IS_CUSTOM_TEMPLATE=1
|
||||||
|
fi
|
||||||
TEMP_PATH=./bin/actions.runner.plist.temp
|
TEMP_PATH=./bin/actions.runner.plist.temp
|
||||||
CONFIG_PATH=.service
|
CONFIG_PATH=.service
|
||||||
|
|
||||||
@@ -29,7 +35,11 @@ function failed()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [ ! -f "${TEMPLATE_PATH}" ]; then
|
if [ ! -f "${TEMPLATE_PATH}" ]; then
|
||||||
failed "Must run from runner root or install is corrupt"
|
if [[ $IS_CUSTOM_TEMPLATE = 0 ]]; then
|
||||||
|
failed "Must run from runner root or install is corrupt"
|
||||||
|
else
|
||||||
|
failed "Service file at '$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE' using GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE env variable is not found"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
function install()
|
function install()
|
||||||
@@ -53,7 +63,7 @@ function install()
|
|||||||
mkdir -p "${log_path}" || failed "failed to create ${log_path}"
|
mkdir -p "${log_path}" || failed "failed to create ${log_path}"
|
||||||
|
|
||||||
echo Creating ${PLIST_PATH}
|
echo Creating ${PLIST_PATH}
|
||||||
sed "s/{{User}}/${SUDO_USER:-$USER}/g; s/{{SvcName}}/$SVC_NAME/g; s@{{RunnerRoot}}@${RUNNER_ROOT}@g; s@{{UserHome}}@$HOME@g;" "${TEMPLATE_PATH}" > "${TEMP_PATH}" || failed "failed to create replacement temp file"
|
sed "s/{{User}}/${USER:-$SUDO_USER}/g; s/{{SvcName}}/$SVC_NAME/g; s@{{RunnerRoot}}@${RUNNER_ROOT}@g; s@{{UserHome}}@$HOME@g;" "${TEMPLATE_PATH}" > "${TEMP_PATH}" || failed "failed to create replacement temp file"
|
||||||
mv "${TEMP_PATH}" "${PLIST_PATH}" || failed "failed to copy plist"
|
mv "${TEMP_PATH}" "${PLIST_PATH}" || failed "failed to copy plist"
|
||||||
|
|
||||||
# Since we started with sudo, runsvc.sh will be owned by root. Change this to current login user.
|
# Since we started with sudo, runsvc.sh will be owned by root. Change this to current login user.
|
||||||
|
|||||||
@@ -43,6 +43,32 @@ module.exports =
|
|||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
/******/ ({
|
/******/ ({
|
||||||
|
|
||||||
|
/***/ 82:
|
||||||
|
/***/ (function(__unusedmodule, exports) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// We use any as a valid input type
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
/**
|
||||||
|
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||||
|
* @param input input to sanitize into a string
|
||||||
|
*/
|
||||||
|
function toCommandValue(input) {
|
||||||
|
if (input === null || input === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
else if (typeof input === 'string' || input instanceof String) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return JSON.stringify(input);
|
||||||
|
}
|
||||||
|
exports.toCommandValue = toCommandValue;
|
||||||
|
//# sourceMappingURL=utils.js.map
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ 87:
|
/***/ 87:
|
||||||
/***/ (function(module) {
|
/***/ (function(module) {
|
||||||
|
|
||||||
@@ -978,6 +1004,42 @@ function regExpEscape (s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 102:
|
||||||
|
/***/ (function(__unusedmodule, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// For internal use, subject to change.
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||||
|
result["default"] = mod;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
// We use any as a valid input type
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
const fs = __importStar(__webpack_require__(747));
|
||||||
|
const os = __importStar(__webpack_require__(87));
|
||||||
|
const utils_1 = __webpack_require__(82);
|
||||||
|
function issueCommand(command, message) {
|
||||||
|
const filePath = process.env[`GITHUB_${command}`];
|
||||||
|
if (!filePath) {
|
||||||
|
throw new Error(`Unable to find environment variable for file command ${command}`);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error(`Missing file at path: ${filePath}`);
|
||||||
|
}
|
||||||
|
fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, {
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.issueCommand = issueCommand;
|
||||||
|
//# sourceMappingURL=file-command.js.map
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 281:
|
/***/ 281:
|
||||||
@@ -1495,12 +1557,12 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const glob = __importStar(__webpack_require__(281));
|
|
||||||
const crypto = __importStar(__webpack_require__(417));
|
const crypto = __importStar(__webpack_require__(417));
|
||||||
const fs = __importStar(__webpack_require__(747));
|
const fs = __importStar(__webpack_require__(747));
|
||||||
|
const glob = __importStar(__webpack_require__(281));
|
||||||
|
const path = __importStar(__webpack_require__(622));
|
||||||
const stream = __importStar(__webpack_require__(413));
|
const stream = __importStar(__webpack_require__(413));
|
||||||
const util = __importStar(__webpack_require__(669));
|
const util = __importStar(__webpack_require__(669));
|
||||||
const path = __importStar(__webpack_require__(622));
|
|
||||||
function run() {
|
function run() {
|
||||||
var e_1, _a;
|
var e_1, _a;
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
@@ -1551,7 +1613,7 @@ function run() {
|
|||||||
}
|
}
|
||||||
result.end();
|
result.end();
|
||||||
if (hasMatch) {
|
if (hasMatch) {
|
||||||
console.log(`Find ${count} files to hash.`);
|
console.log(`Found ${count} files to hash.`);
|
||||||
console.error(`__OUTPUT__${result.digest('hex')}__OUTPUT__`);
|
console.error(`__OUTPUT__${result.digest('hex')}__OUTPUT__`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -1559,7 +1621,15 @@ function run() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
run();
|
run()
|
||||||
|
.then(out => {
|
||||||
|
console.log(out);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
@@ -1687,17 +1757,25 @@ module.exports = require("crypto");
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||||
|
result["default"] = mod;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const os = __webpack_require__(87);
|
const os = __importStar(__webpack_require__(87));
|
||||||
|
const utils_1 = __webpack_require__(82);
|
||||||
/**
|
/**
|
||||||
* Commands
|
* Commands
|
||||||
*
|
*
|
||||||
* Command Format:
|
* Command Format:
|
||||||
* ##[name key=value;key=value]message
|
* ::name key=value,key=value::message
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
* ##[warning]This is the user warning message
|
* ::warning::This is the message
|
||||||
* ##[set-secret name=mypassword]definitelyNotAPassword!
|
* ::set-env name=MY_VAR::some value
|
||||||
*/
|
*/
|
||||||
function issueCommand(command, properties, message) {
|
function issueCommand(command, properties, message) {
|
||||||
const cmd = new Command(command, properties, message);
|
const cmd = new Command(command, properties, message);
|
||||||
@@ -1722,34 +1800,39 @@ class Command {
|
|||||||
let cmdStr = CMD_STRING + this.command;
|
let cmdStr = CMD_STRING + this.command;
|
||||||
if (this.properties && Object.keys(this.properties).length > 0) {
|
if (this.properties && Object.keys(this.properties).length > 0) {
|
||||||
cmdStr += ' ';
|
cmdStr += ' ';
|
||||||
|
let first = true;
|
||||||
for (const key in this.properties) {
|
for (const key in this.properties) {
|
||||||
if (this.properties.hasOwnProperty(key)) {
|
if (this.properties.hasOwnProperty(key)) {
|
||||||
const val = this.properties[key];
|
const val = this.properties[key];
|
||||||
if (val) {
|
if (val) {
|
||||||
// safely append the val - avoid blowing up when attempting to
|
if (first) {
|
||||||
// call .replace() if message is not a string for some reason
|
first = false;
|
||||||
cmdStr += `${key}=${escape(`${val || ''}`)},`;
|
}
|
||||||
|
else {
|
||||||
|
cmdStr += ',';
|
||||||
|
}
|
||||||
|
cmdStr += `${key}=${escapeProperty(val)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmdStr += CMD_STRING;
|
cmdStr += `${CMD_STRING}${escapeData(this.message)}`;
|
||||||
// safely append the message - avoid blowing up when attempting to
|
|
||||||
// call .replace() if message is not a string for some reason
|
|
||||||
const message = `${this.message || ''}`;
|
|
||||||
cmdStr += escapeData(message);
|
|
||||||
return cmdStr;
|
return cmdStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function escapeData(s) {
|
function escapeData(s) {
|
||||||
return s.replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
return utils_1.toCommandValue(s)
|
||||||
|
.replace(/%/g, '%25')
|
||||||
|
.replace(/\r/g, '%0D')
|
||||||
|
.replace(/\n/g, '%0A');
|
||||||
}
|
}
|
||||||
function escape(s) {
|
function escapeProperty(s) {
|
||||||
return s
|
return utils_1.toCommandValue(s)
|
||||||
|
.replace(/%/g, '%25')
|
||||||
.replace(/\r/g, '%0D')
|
.replace(/\r/g, '%0D')
|
||||||
.replace(/\n/g, '%0A')
|
.replace(/\n/g, '%0A')
|
||||||
.replace(/]/g, '%5D')
|
.replace(/:/g, '%3A')
|
||||||
.replace(/;/g, '%3B');
|
.replace(/,/g, '%2C');
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=command.js.map
|
//# sourceMappingURL=command.js.map
|
||||||
|
|
||||||
@@ -1769,10 +1852,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||||
|
result["default"] = mod;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const command_1 = __webpack_require__(431);
|
const command_1 = __webpack_require__(431);
|
||||||
const os = __webpack_require__(87);
|
const file_command_1 = __webpack_require__(102);
|
||||||
const path = __webpack_require__(622);
|
const utils_1 = __webpack_require__(82);
|
||||||
|
const os = __importStar(__webpack_require__(87));
|
||||||
|
const path = __importStar(__webpack_require__(622));
|
||||||
/**
|
/**
|
||||||
* The code to exit an action
|
* The code to exit an action
|
||||||
*/
|
*/
|
||||||
@@ -1793,11 +1885,21 @@ var ExitCode;
|
|||||||
/**
|
/**
|
||||||
* Sets env variable for this action and future actions in the job
|
* Sets env variable for this action and future actions in the job
|
||||||
* @param name the name of the variable to set
|
* @param name the name of the variable to set
|
||||||
* @param val the value of the variable
|
* @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function exportVariable(name, val) {
|
function exportVariable(name, val) {
|
||||||
process.env[name] = val;
|
const convertedVal = utils_1.toCommandValue(val);
|
||||||
command_1.issueCommand('set-env', { name }, val);
|
process.env[name] = convertedVal;
|
||||||
|
const filePath = process.env['GITHUB_ENV'] || '';
|
||||||
|
if (filePath) {
|
||||||
|
const delimiter = '_GitHubActionsFileCommandDelimeter_';
|
||||||
|
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`;
|
||||||
|
file_command_1.issueCommand('ENV', commandValue);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
command_1.issueCommand('set-env', { name }, convertedVal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
exports.exportVariable = exportVariable;
|
exports.exportVariable = exportVariable;
|
||||||
/**
|
/**
|
||||||
@@ -1813,7 +1915,13 @@ exports.setSecret = setSecret;
|
|||||||
* @param inputPath
|
* @param inputPath
|
||||||
*/
|
*/
|
||||||
function addPath(inputPath) {
|
function addPath(inputPath) {
|
||||||
command_1.issueCommand('add-path', {}, inputPath);
|
const filePath = process.env['GITHUB_PATH'] || '';
|
||||||
|
if (filePath) {
|
||||||
|
file_command_1.issueCommand('PATH', inputPath);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
command_1.issueCommand('add-path', {}, inputPath);
|
||||||
|
}
|
||||||
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
|
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
|
||||||
}
|
}
|
||||||
exports.addPath = addPath;
|
exports.addPath = addPath;
|
||||||
@@ -1836,12 +1944,22 @@ exports.getInput = getInput;
|
|||||||
* Sets the value of an output.
|
* Sets the value of an output.
|
||||||
*
|
*
|
||||||
* @param name name of the output to set
|
* @param name name of the output to set
|
||||||
* @param value value to store
|
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function setOutput(name, value) {
|
function setOutput(name, value) {
|
||||||
command_1.issueCommand('set-output', { name }, value);
|
command_1.issueCommand('set-output', { name }, value);
|
||||||
}
|
}
|
||||||
exports.setOutput = setOutput;
|
exports.setOutput = setOutput;
|
||||||
|
/**
|
||||||
|
* Enables or disables the echoing of commands into stdout for the rest of the step.
|
||||||
|
* Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function setCommandEcho(enabled) {
|
||||||
|
command_1.issue('echo', enabled ? 'on' : 'off');
|
||||||
|
}
|
||||||
|
exports.setCommandEcho = setCommandEcho;
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
// Results
|
// Results
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
@@ -1858,6 +1976,13 @@ exports.setFailed = setFailed;
|
|||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
// Logging Commands
|
// Logging Commands
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
|
/**
|
||||||
|
* Gets whether Actions Step Debug is on or not
|
||||||
|
*/
|
||||||
|
function isDebug() {
|
||||||
|
return process.env['RUNNER_DEBUG'] === '1';
|
||||||
|
}
|
||||||
|
exports.isDebug = isDebug;
|
||||||
/**
|
/**
|
||||||
* Writes debug message to user log
|
* Writes debug message to user log
|
||||||
* @param message debug message
|
* @param message debug message
|
||||||
@@ -1868,18 +1993,18 @@ function debug(message) {
|
|||||||
exports.debug = debug;
|
exports.debug = debug;
|
||||||
/**
|
/**
|
||||||
* Adds an error issue
|
* Adds an error issue
|
||||||
* @param message error issue message
|
* @param message error issue message. Errors will be converted to string via toString()
|
||||||
*/
|
*/
|
||||||
function error(message) {
|
function error(message) {
|
||||||
command_1.issue('error', message);
|
command_1.issue('error', message instanceof Error ? message.toString() : message);
|
||||||
}
|
}
|
||||||
exports.error = error;
|
exports.error = error;
|
||||||
/**
|
/**
|
||||||
* Adds an warning issue
|
* Adds an warning issue
|
||||||
* @param message warning issue message
|
* @param message warning issue message. Errors will be converted to string via toString()
|
||||||
*/
|
*/
|
||||||
function warning(message) {
|
function warning(message) {
|
||||||
command_1.issue('warning', message);
|
command_1.issue('warning', message instanceof Error ? message.toString() : message);
|
||||||
}
|
}
|
||||||
exports.warning = warning;
|
exports.warning = warning;
|
||||||
/**
|
/**
|
||||||
@@ -1937,8 +2062,9 @@ exports.group = group;
|
|||||||
* Saves state for current action, the state can only be retrieved by this action's post job execution.
|
* Saves state for current action, the state can only be retrieved by this action's post job execution.
|
||||||
*
|
*
|
||||||
* @param name name of the state to store
|
* @param name name of the state to store
|
||||||
* @param value value to store
|
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function saveState(name, value) {
|
function saveState(name, value) {
|
||||||
command_1.issueCommand('save-state', { name }, value);
|
command_1.issueCommand('save-state', { name }, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ if [ -f ".path" ]; then
|
|||||||
echo ".path=${PATH}"
|
echo ".path=${PATH}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# insert anything to setup env when running as a service
|
nodever=${GITHUB_ACTIONS_RUNNER_FORCED_NODE_VERSION:-node16}
|
||||||
|
|
||||||
|
# insert anything to setup env when running as a service
|
||||||
# run the host process which keep the listener alive
|
# run the host process which keep the listener alive
|
||||||
./externals/node12/bin/node ./bin/RunnerService.js &
|
./externals/$nodever/bin/node ./bin/RunnerService.js &
|
||||||
PID=$!
|
PID=$!
|
||||||
wait $PID
|
wait $PID
|
||||||
trap - TERM INT
|
trap - TERM INT
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ arg_2=${2}
|
|||||||
RUNNER_ROOT=`pwd`
|
RUNNER_ROOT=`pwd`
|
||||||
|
|
||||||
UNIT_PATH=/etc/systemd/system/${SVC_NAME}
|
UNIT_PATH=/etc/systemd/system/${SVC_NAME}
|
||||||
TEMPLATE_PATH=./bin/actions.runner.service.template
|
TEMPLATE_PATH=$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE
|
||||||
|
IS_CUSTOM_TEMPLATE=0
|
||||||
|
if [[ -z $TEMPLATE_PATH ]]; then
|
||||||
|
TEMPLATE_PATH=./bin/actions.runner.service.template
|
||||||
|
else
|
||||||
|
IS_CUSTOM_TEMPLATE=1
|
||||||
|
fi
|
||||||
TEMP_PATH=./bin/actions.runner.service.temp
|
TEMP_PATH=./bin/actions.runner.service.temp
|
||||||
CONFIG_PATH=.service
|
CONFIG_PATH=.service
|
||||||
|
|
||||||
@@ -31,7 +37,11 @@ function failed()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [ ! -f "${TEMPLATE_PATH}" ]; then
|
if [ ! -f "${TEMPLATE_PATH}" ]; then
|
||||||
failed "Must run from runner root or install is corrupt"
|
if [[ $IS_CUSTOM_TEMPLATE = 0 ]]; then
|
||||||
|
failed "Must run from runner root or install is corrupt"
|
||||||
|
else
|
||||||
|
failed "Service file at '$GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE' using GITHUB_ACTIONS_RUNNER_SERVICE_TEMPLATE env variable is not found"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#check if we run as root
|
#check if we run as root
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ date "+[%F %T-%4N] Waiting for $runnerprocessname ($runnerpid) to complete" >> "
|
|||||||
while [ -e /proc/$runnerpid ]
|
while [ -e /proc/$runnerpid ]
|
||||||
do
|
do
|
||||||
date "+[%F %T-%4N] Process $runnerpid still running" >> "$logfile" 2>&1
|
date "+[%F %T-%4N] Process $runnerpid still running" >> "$logfile" 2>&1
|
||||||
sleep 2
|
"$rootfolder"/safe_sleep.sh 2
|
||||||
done
|
done
|
||||||
date "+[%F %T-%4N] Process $runnerpid finished running" >> "$logfile" 2>&1
|
date "+[%F %T-%4N] Process $runnerpid finished running" >> "$logfile" 2>&1
|
||||||
|
|
||||||
# start re-organize folders
|
# start re-organize folders
|
||||||
date "+[%F %T-%4N] Sleep 1 more second to make sure process exited" >> "$logfile" 2>&1
|
date "+[%F %T-%4N] Sleep 1 more second to make sure process exited" >> "$logfile" 2>&1
|
||||||
sleep 1
|
"$rootfolder"/safe_sleep.sh 1
|
||||||
|
|
||||||
# the folder structure under runner root will be
|
# the folder structure under runner root will be
|
||||||
# ./bin -> bin.2.100.0 (junction folder)
|
# ./bin -> bin.2.100.0 (junction folder)
|
||||||
@@ -125,7 +125,7 @@ attemptedtargetedfix=0
|
|||||||
currentplatform=$(uname | awk '{print tolower($0)}')
|
currentplatform=$(uname | awk '{print tolower($0)}')
|
||||||
if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
||||||
# We needed a fix for https://github.com/actions/runner/issues/743
|
# We needed a fix for https://github.com/actions/runner/issues/743
|
||||||
# We will recreate the ./externals/node12/bin/node of the past runner version that launched the runnerlistener service
|
# We will recreate the ./externals/nodeXY/bin/node of the past runner version that launched the runnerlistener service
|
||||||
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
|
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
|
||||||
|
|
||||||
# We need the pid for the nodejs loop, get that here, its the parent of the runner C# pid
|
# We need the pid for the nodejs loop, get that here, its the parent of the runner C# pid
|
||||||
@@ -135,7 +135,13 @@ if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
|||||||
then
|
then
|
||||||
# inspect the open file handles to find the node process
|
# inspect the open file handles to find the node process
|
||||||
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
|
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
|
||||||
path=$(lsof -a -g "$procgroup" -F n | grep node12/bin/node | grep externals | tail -1 | cut -c2-)
|
nodever="node16"
|
||||||
|
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
|
||||||
|
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node12
|
||||||
|
then
|
||||||
|
nodever="node12"
|
||||||
|
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
|
||||||
|
fi
|
||||||
if [[ $? -eq 0 && -n "$path" ]]
|
if [[ $? -eq 0 && -n "$path" ]]
|
||||||
then
|
then
|
||||||
# trim the last 5 characters of the path '/node'
|
# trim the last 5 characters of the path '/node'
|
||||||
@@ -148,7 +154,7 @@ if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
|||||||
then
|
then
|
||||||
date "+[%F %T-%4N] Creating fallback node at path $path" >> "$logfile" 2>&1
|
date "+[%F %T-%4N] Creating fallback node at path $path" >> "$logfile" 2>&1
|
||||||
mkdir -p "$trimmedpath"
|
mkdir -p "$trimmedpath"
|
||||||
cp "$rootfolder/externals/node12/bin/node" "$path"
|
cp "$rootfolder/externals/$nodever/bin/node" "$path"
|
||||||
else
|
else
|
||||||
date "+[%F %T-%4N] Path for fallback node exists, skipping creating $path" >> "$logfile" 2>&1
|
date "+[%F %T-%4N] Path for fallback node exists, skipping creating $path" >> "$logfile" 2>&1
|
||||||
fi
|
fi
|
||||||
|
|||||||
39
src/Misc/layoutroot/run-helper.cmd.template
Normal file
39
src/Misc/layoutroot/run-helper.cmd.template
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
"%~dp0\bin\Runner.Listener.exe" run %*
|
||||||
|
|
||||||
|
rem using `if %ERRORLEVEL% EQU N` insterad of `if ERRORLEVEL N`
|
||||||
|
rem `if ERRORLEVEL N` means: error level is N or MORE
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 0 (
|
||||||
|
echo "Runner listener exit with 0 return code, stop the service, no retry needed."
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 1 (
|
||||||
|
echo "Runner listener exit with terminated error, stop the service, no retry needed."
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 2 (
|
||||||
|
echo "Runner listener exit with retryable error, re-launch runner in 5 seconds."
|
||||||
|
ping 127.0.0.1 -n 6 -w 1000 >NUL
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 3 (
|
||||||
|
rem Sleep 5 seconds to wait for the runner update process finish
|
||||||
|
echo "Runner listener exit because of updating, re-launch runner in 5 seconds"
|
||||||
|
ping 127.0.0.1 -n 6 -w 1000 >NUL
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 4 (
|
||||||
|
rem Sleep 5 seconds to wait for the ephemeral runner update process finish
|
||||||
|
echo "Runner listener exit because of updating, re-launch ephemeral runner in 5 seconds"
|
||||||
|
ping 127.0.0.1 -n 6 -w 1000 >NUL
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Exiting after unknown error code: %ERRORLEVEL%"
|
||||||
|
exit /b 0
|
||||||
46
src/Misc/layoutroot/run-helper.sh.template
Executable file
46
src/Misc/layoutroot/run-helper.sh.template
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Validate not sudo
|
||||||
|
user_id=`id -u`
|
||||||
|
if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then
|
||||||
|
echo "Must not run interactively with sudo"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run
|
||||||
|
shopt -s nocasematch
|
||||||
|
|
||||||
|
SOURCE="${BASH_SOURCE[0]}"
|
||||||
|
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
|
||||||
|
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||||
|
SOURCE="$(readlink "$SOURCE")"
|
||||||
|
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
|
||||||
|
done
|
||||||
|
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||||
|
"$DIR"/bin/Runner.Listener run $*
|
||||||
|
|
||||||
|
returnCode=$?
|
||||||
|
if [[ $returnCode == 0 ]]; then
|
||||||
|
echo "Runner listener exit with 0 return code, stop the service, no retry needed."
|
||||||
|
exit 0
|
||||||
|
elif [[ $returnCode == 1 ]]; then
|
||||||
|
echo "Runner listener exit with terminated error, stop the service, no retry needed."
|
||||||
|
exit 0
|
||||||
|
elif [[ $returnCode == 2 ]]; then
|
||||||
|
echo "Runner listener exit with retryable error, re-launch runner in 5 seconds."
|
||||||
|
"$DIR"/safe_sleep.sh 5
|
||||||
|
exit 2
|
||||||
|
elif [[ $returnCode == 3 ]]; then
|
||||||
|
# Sleep 5 seconds to wait for the runner update process finish
|
||||||
|
echo "Runner listener exit because of updating, re-launch runner in 5 seconds"
|
||||||
|
"$DIR"/safe_sleep.sh 5
|
||||||
|
exit 2
|
||||||
|
elif [[ $returnCode == 4 ]]; then
|
||||||
|
# Sleep 5 seconds to wait for the ephemeral runner update process finish
|
||||||
|
echo "Runner listener exit because of updating, re-launch ephemeral runner in 5 seconds"
|
||||||
|
"$DIR"/safe_sleep.sh 5
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
echo "Exiting with unknown error code: ${returnCode}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
@@ -13,21 +13,19 @@ if defined VERBOSE_ARG (
|
|||||||
rem Unblock files in the root of the layout folder. E.g. .cmd files.
|
rem Unblock files in the root of the layout folder. E.g. .cmd files.
|
||||||
powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "$VerbosePreference = %VERBOSE_ARG% ; Get-ChildItem -LiteralPath '%~dp0' | ForEach-Object { Write-Verbose ('Unblock: {0}' -f $_.FullName) ; $_ } | Unblock-File | Out-Null"
|
powershell.exe -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "$VerbosePreference = %VERBOSE_ARG% ; Get-ChildItem -LiteralPath '%~dp0' | ForEach-Object { Write-Verbose ('Unblock: {0}' -f $_.FullName) ; $_ } | Unblock-File | Out-Null"
|
||||||
|
|
||||||
if /i "%~1" equ "localRun" (
|
|
||||||
rem ********************************************************************************
|
|
||||||
rem Local run.
|
|
||||||
rem ********************************************************************************
|
|
||||||
"%~dp0bin\Runner.Listener.exe" %*
|
|
||||||
) else (
|
|
||||||
rem ********************************************************************************
|
|
||||||
rem Run.
|
|
||||||
rem ********************************************************************************
|
|
||||||
"%~dp0bin\Runner.Listener.exe" run %*
|
|
||||||
|
|
||||||
rem Return code 4 means the run once runner received an update message.
|
rem ********************************************************************************
|
||||||
rem Sleep 5 seconds to wait for the update process finish and run the runner again.
|
rem Run.
|
||||||
if ERRORLEVEL 4 (
|
rem ********************************************************************************
|
||||||
timeout /t 5 /nobreak > NUL
|
|
||||||
"%~dp0bin\Runner.Listener.exe" run %*
|
:launch_helper
|
||||||
)
|
copy "%~dp0run-helper.cmd.template" "%~dp0run-helper.cmd" /Y
|
||||||
|
call "%~dp0run-helper.cmd" %*
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 1 (
|
||||||
|
echo "Restarting runner..."
|
||||||
|
goto :launch_helper
|
||||||
|
) else (
|
||||||
|
echo "Exiting runner..."
|
||||||
|
exit /b 0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,64 +1,24 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Validate not sudo
|
|
||||||
user_id=`id -u`
|
|
||||||
if [ $user_id -eq 0 -a -z "$RUNNER_ALLOW_RUNASROOT" ]; then
|
|
||||||
echo "Must not run interactively with sudo"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Change directory to the script root directory
|
# Change directory to the script root directory
|
||||||
# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within
|
# https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within
|
||||||
SOURCE="${BASH_SOURCE[0]}"
|
SOURCE="${BASH_SOURCE[0]}"
|
||||||
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
|
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
|
||||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||||
SOURCE="$(readlink "$SOURCE")"
|
SOURCE="$(readlink "$SOURCE")"
|
||||||
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
|
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
|
||||||
done
|
done
|
||||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||||
|
cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh
|
||||||
# Do not "cd $DIR". For localRun, the current directory is expected to be the repo location on disk.
|
# run the helper process which keep the listener alive
|
||||||
|
while :;
|
||||||
# Run
|
do
|
||||||
shopt -s nocasematch
|
"$DIR"/run-helper.sh $*
|
||||||
if [[ "$1" == "localRun" ]]; then
|
|
||||||
"$DIR"/bin/Runner.Listener $*
|
|
||||||
else
|
|
||||||
"$DIR"/bin/Runner.Listener run $*
|
|
||||||
|
|
||||||
# Return code 3 means the run once runner received an update message.
|
|
||||||
# Sleep 5 seconds to wait for the update process finish
|
|
||||||
returnCode=$?
|
returnCode=$?
|
||||||
if [[ $returnCode == 3 ]]; then
|
if [[ $returnCode -eq 2 ]]; then
|
||||||
if [ ! -x "$(command -v sleep)" ]; then
|
echo "Restarting runner..."
|
||||||
if [ ! -x "$(command -v ping)" ]; then
|
|
||||||
COUNT="0"
|
|
||||||
while [[ $COUNT != 5000 ]]; do
|
|
||||||
echo "SLEEP" > /dev/null
|
|
||||||
COUNT=$[$COUNT+1]
|
|
||||||
done
|
|
||||||
else
|
|
||||||
ping -c 5 127.0.0.1 > /dev/null
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
sleep 5
|
|
||||||
fi
|
|
||||||
elif [[ $returnCode == 4 ]]; then
|
|
||||||
if [ ! -x "$(command -v sleep)" ]; then
|
|
||||||
if [ ! -x "$(command -v ping)" ]; then
|
|
||||||
COUNT="0"
|
|
||||||
while [[ $COUNT != 5000 ]]; do
|
|
||||||
echo "SLEEP" > /dev/null
|
|
||||||
COUNT=$[$COUNT+1]
|
|
||||||
done
|
|
||||||
else
|
|
||||||
ping -c 5 127.0.0.1 > /dev/null
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
sleep 5
|
|
||||||
fi
|
|
||||||
"$DIR"/bin/Runner.Listener run $*
|
|
||||||
else
|
else
|
||||||
exit $returnCode
|
echo "Exiting runner..."
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
done
|
||||||
|
|||||||
6
src/Misc/layoutroot/safe_sleep.sh
Normal file
6
src/Misc/layoutroot/safe_sleep.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SECONDS=0
|
||||||
|
while [[ $SECONDS != $1 ]]; do
|
||||||
|
:
|
||||||
|
done
|
||||||
@@ -33,6 +33,9 @@ namespace GitHub.Runner.Common
|
|||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public string PoolName { get; set; }
|
public string PoolName { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public bool DisableUpdate { get; set; }
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public bool Ephemeral { get; set; }
|
public bool Ephemeral { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ namespace GitHub.Runner.Common
|
|||||||
public static class CommandLine
|
public static class CommandLine
|
||||||
{
|
{
|
||||||
//if you are adding a new arg, please make sure you update the
|
//if you are adding a new arg, please make sure you update the
|
||||||
//validArgs array as well present in the CommandSettings.cs
|
//validOptions dictionary as well present in the CommandSettings.cs
|
||||||
public static class Args
|
public static class Args
|
||||||
{
|
{
|
||||||
public static readonly string Auth = "auth";
|
public static readonly string Auth = "auth";
|
||||||
@@ -121,7 +121,7 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
//if you are adding a new flag, please make sure you update the
|
//if you are adding a new flag, please make sure you update the
|
||||||
//validFlags array as well present in the CommandSettings.cs
|
//validOptions dictionary as well present in the CommandSettings.cs
|
||||||
public static class Flags
|
public static class Flags
|
||||||
{
|
{
|
||||||
public static readonly string Check = "check";
|
public static readonly string Check = "check";
|
||||||
@@ -129,6 +129,7 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string Ephemeral = "ephemeral";
|
public static readonly string Ephemeral = "ephemeral";
|
||||||
public static readonly string Help = "help";
|
public static readonly string Help = "help";
|
||||||
public static readonly string Replace = "replace";
|
public static readonly string Replace = "replace";
|
||||||
|
public static readonly string DisableUpdate = "disableupdate";
|
||||||
public static readonly string Once = "once"; // Keep this around since customers still relies on it
|
public static readonly string Once = "once"; // Keep this around since customers still relies on it
|
||||||
public static readonly string RunAsService = "runasservice";
|
public static readonly string RunAsService = "runasservice";
|
||||||
public static readonly string Unattended = "unattended";
|
public static readonly string Unattended = "unattended";
|
||||||
@@ -148,6 +149,8 @@ namespace GitHub.Runner.Common
|
|||||||
public static class Features
|
public static class Features
|
||||||
{
|
{
|
||||||
public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
|
public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
|
||||||
|
public static readonly string Node12Warning = "DistributedTask.AddWarningToNode12Action";
|
||||||
|
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
||||||
@@ -155,7 +158,9 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string LowDiskSpace = "LOW_DISK_SPACE";
|
public static readonly string LowDiskSpace = "LOW_DISK_SPACE";
|
||||||
public static readonly string UnsupportedCommand = "UNSUPPORTED_COMMAND";
|
public static readonly string UnsupportedCommand = "UNSUPPORTED_COMMAND";
|
||||||
public static readonly string UnsupportedCommandMessageDisabled = "The `{0}` command is disabled. Please upgrade to using Environment Files or opt into unsecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/";
|
public static readonly string UnsupportedCommandMessageDisabled = "The `{0}` command is disabled. Please upgrade to using Environment Files or opt into unsecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/";
|
||||||
public static readonly string UnsupportedStopCommandTokenDisabled = "You cannot use a endToken that is an empty string, the string 'pause-logging', or another workflow command. For more information see: https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#example-stopping-and-starting-workflow-commands or opt into insecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS` environment variable to `true`.";
|
public static readonly string UnsupportedStopCommandTokenDisabled = "You cannot use a endToken that is an empty string, the string 'pause-logging', or another workflow command. For more information see: https://docs.github.com/actions/learn-github-actions/workflow-commands-for-github-actions#example-stopping-and-starting-workflow-commands or opt into insecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS` environment variable to `true`.";
|
||||||
|
public static readonly string UnsupportedSummarySize = "$GITHUB_STEP_SUMMARY upload aborted, supports content up to a size of {0}k, got {1}k. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary";
|
||||||
|
public static readonly string Node12DetectedAfterEndOfLife = "Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: {0}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class RunnerEvent
|
public static class RunnerEvent
|
||||||
@@ -187,6 +192,12 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string Success = "success";
|
public static readonly string Success = "success";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Hooks
|
||||||
|
{
|
||||||
|
public static readonly string JobStartedStepName = "Set up runner";
|
||||||
|
public static readonly string JobCompletedStepName = "Complete runner";
|
||||||
|
}
|
||||||
|
|
||||||
public static class Path
|
public static class Path
|
||||||
{
|
{
|
||||||
public static readonly string ActionsDirectory = "_actions";
|
public static readonly string ActionsDirectory = "_actions";
|
||||||
@@ -218,11 +229,15 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS";
|
public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS";
|
||||||
public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG";
|
public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG";
|
||||||
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
|
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
|
||||||
|
public static readonly string AllowActionsUseUnsecureNodeVersion = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Agent
|
public static class Agent
|
||||||
{
|
{
|
||||||
public static readonly string ToolsDirectory = "agent.ToolsDirectory";
|
public static readonly string ToolsDirectory = "agent.ToolsDirectory";
|
||||||
|
|
||||||
|
// Set this env var to "node12" to downgrade the node version for internal functions (e.g hashfiles). This does NOT affect the version of node actions.
|
||||||
|
public static readonly string ForcedInternalNodeVersion = "ACTIONS_RUNNER_FORCED_INTERNAL_NODE_VERSION";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class System
|
public static class System
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ namespace GitHub.Runner.Common
|
|||||||
case "GitHub.Runner.Worker.IFileCommandExtension":
|
case "GitHub.Runner.Worker.IFileCommandExtension":
|
||||||
Add<T>(extensions, "GitHub.Runner.Worker.AddPathFileCommand, Runner.Worker");
|
Add<T>(extensions, "GitHub.Runner.Worker.AddPathFileCommand, Runner.Worker");
|
||||||
Add<T>(extensions, "GitHub.Runner.Worker.SetEnvFileCommand, Runner.Worker");
|
Add<T>(extensions, "GitHub.Runner.Worker.SetEnvFileCommand, Runner.Worker");
|
||||||
|
Add<T>(extensions, "GitHub.Runner.Worker.CreateStepSummaryCommand, Runner.Worker");
|
||||||
break;
|
break;
|
||||||
case "GitHub.Runner.Listener.Check.ICheckExtension":
|
case "GitHub.Runner.Listener.Check.ICheckExtension":
|
||||||
Add<T>(extensions, "GitHub.Runner.Listener.Check.InternetCheck, Runner.Listener");
|
Add<T>(extensions, "GitHub.Runner.Listener.Check.InternetCheck, Runner.Listener");
|
||||||
|
|||||||
@@ -193,6 +193,11 @@ namespace GitHub.Runner.Common
|
|||||||
_trace.Info($"No proxy settings were found based on environmental variables (http_proxy/https_proxy/HTTP_PROXY/HTTPS_PROXY)");
|
_trace.Info($"No proxy settings were found based on environmental variables (http_proxy/https_proxy/HTTP_PROXY/HTTPS_PROXY)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
|
||||||
|
{
|
||||||
|
_trace.Warning($"Runner is running under insecure mode: HTTPS server certifcate validation has been turned off by GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY environment variable.");
|
||||||
|
}
|
||||||
|
|
||||||
var credFile = GetConfigFile(WellKnownConfigFile.Credentials);
|
var credFile = GetConfigFile(WellKnownConfigFile.Credentials);
|
||||||
if (File.Exists(credFile))
|
if (File.Exists(credFile))
|
||||||
{
|
{
|
||||||
@@ -211,6 +216,8 @@ namespace GitHub.Runner.Common
|
|||||||
_userAgents.Add(new ProductInfoHeaderValue("RunnerId", runnerSettings.AgentId.ToString(CultureInfo.InvariantCulture)));
|
_userAgents.Add(new ProductInfoHeaderValue("RunnerId", runnerSettings.AgentId.ToString(CultureInfo.InvariantCulture)));
|
||||||
_userAgents.Add(new ProductInfoHeaderValue("GroupId", runnerSettings.PoolId.ToString(CultureInfo.InvariantCulture)));
|
_userAgents.Add(new ProductInfoHeaderValue("GroupId", runnerSettings.PoolId.ToString(CultureInfo.InvariantCulture)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_userAgents.Add(new ProductInfoHeaderValue("CommitSHA", BuildConstants.Source.CommitHash));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetDirectory(WellKnownDirectory directory)
|
public string GetDirectory(WellKnownDirectory directory)
|
||||||
@@ -350,7 +357,7 @@ namespace GitHub.Runner.Common
|
|||||||
GetDirectory(WellKnownDirectory.Root),
|
GetDirectory(WellKnownDirectory.Root),
|
||||||
".setup_info");
|
".setup_info");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WellKnownConfigFile.Telemetry:
|
case WellKnownConfigFile.Telemetry:
|
||||||
path = Path.Combine(
|
path = Path.Combine(
|
||||||
GetDirectory(WellKnownDirectory.Diag),
|
GetDirectory(WellKnownDirectory.Diag),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
|
||||||
@@ -13,7 +14,14 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
public HttpClientHandler CreateClientHandler(RunnerWebProxy webProxy)
|
public HttpClientHandler CreateClientHandler(RunnerWebProxy webProxy)
|
||||||
{
|
{
|
||||||
return new HttpClientHandler() { Proxy = webProxy };
|
var client = new HttpClientHandler() { Proxy = webProxy };
|
||||||
|
|
||||||
|
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
|
||||||
|
{
|
||||||
|
client.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,39 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
|
using GitHub.Services.WebApi.Utilities.Internal;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
namespace GitHub.Runner.Common
|
||||||
{
|
{
|
||||||
[ServiceLocator(Default = typeof(JobServer))]
|
[ServiceLocator(Default = typeof(JobServer))]
|
||||||
public interface IJobServer : IRunnerService
|
public interface IJobServer : IRunnerService, IAsyncDisposable
|
||||||
{
|
{
|
||||||
Task ConnectAsync(VssConnection jobConnection);
|
Task ConnectAsync(VssConnection jobConnection);
|
||||||
|
|
||||||
|
void InitializeWebsocketClient(ServiceEndpoint serviceEndpoint);
|
||||||
|
|
||||||
// logging and console
|
// logging and console
|
||||||
Task<TaskLog> AppendLogContentAsync(Guid scopeIdentifier, string hubName, Guid planId, int logId, Stream uploadStream, CancellationToken cancellationToken);
|
Task<TaskLog> AppendLogContentAsync(Guid scopeIdentifier, string hubName, Guid planId, int logId, Stream uploadStream, CancellationToken cancellationToken);
|
||||||
Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, CancellationToken cancellationToken);
|
Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, long? startLine, CancellationToken cancellationToken);
|
||||||
Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, long startLine, CancellationToken cancellationToken);
|
|
||||||
Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, String type, String name, Stream uploadStream, CancellationToken cancellationToken);
|
Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, String type, String name, Stream uploadStream, CancellationToken cancellationToken);
|
||||||
Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken);
|
Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken);
|
||||||
Task<Timeline> CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken);
|
Task<Timeline> CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken);
|
||||||
Task<List<TimelineRecord>> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken);
|
Task<List<TimelineRecord>> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken);
|
||||||
Task RaisePlanEventAsync<T>(Guid scopeIdentifier, string hubName, Guid planId, T eventData, CancellationToken cancellationToken) where T : JobEvent;
|
Task RaisePlanEventAsync<T>(Guid scopeIdentifier, string hubName, Guid planId, T eventData, CancellationToken cancellationToken) where T : JobEvent;
|
||||||
Task<Timeline> GetTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken);
|
Task<Timeline> GetTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken);
|
||||||
Task<ActionDownloadInfoCollection> ResolveActionDownloadInfoAsync(Guid scopeIdentifier, string hubName, Guid planId, ActionReferenceList actions, CancellationToken cancellationToken);
|
Task<ActionDownloadInfoCollection> ResolveActionDownloadInfoAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class JobServer : RunnerService, IJobServer
|
public sealed class JobServer : RunnerService, IJobServer
|
||||||
@@ -34,6 +41,20 @@ namespace GitHub.Runner.Common
|
|||||||
private bool _hasConnection;
|
private bool _hasConnection;
|
||||||
private VssConnection _connection;
|
private VssConnection _connection;
|
||||||
private TaskHttpClient _taskClient;
|
private TaskHttpClient _taskClient;
|
||||||
|
private ClientWebSocket _websocketClient;
|
||||||
|
|
||||||
|
private ServiceEndpoint _serviceEndpoint;
|
||||||
|
|
||||||
|
private int totalBatchedLinesAttemptedByWebsocket = 0;
|
||||||
|
private int failedAttemptsToPostBatchedLinesByWebsocket = 0;
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly TimeSpan _minDelayForWebsocketReconnect = TimeSpan.FromMilliseconds(100);
|
||||||
|
private static readonly TimeSpan _maxDelayForWebsocketReconnect = TimeSpan.FromMilliseconds(500);
|
||||||
|
private static readonly int _minWebsocketFailurePercentageAllowed = 50;
|
||||||
|
private static readonly int _minWebsocketBatchedLinesCountToConsider = 5;
|
||||||
|
|
||||||
|
private Task _websocketConnectTask;
|
||||||
|
|
||||||
public async Task ConnectAsync(VssConnection jobConnection)
|
public async Task ConnectAsync(VssConnection jobConnection)
|
||||||
{
|
{
|
||||||
@@ -42,7 +63,7 @@ namespace GitHub.Runner.Common
|
|||||||
int attemptCount = totalAttempts;
|
int attemptCount = totalAttempts;
|
||||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
||||||
var runnerSettings = configurationStore.GetSettings();
|
var runnerSettings = configurationStore.GetSettings();
|
||||||
|
|
||||||
while (!_connection.HasAuthenticated && attemptCount-- > 0)
|
while (!_connection.HasAuthenticated && attemptCount-- > 0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -117,6 +138,21 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void InitializeWebsocketClient(ServiceEndpoint serviceEndpoint)
|
||||||
|
{
|
||||||
|
this._serviceEndpoint = serviceEndpoint;
|
||||||
|
InitializeWebsocketClient(TimeSpan.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
CloseWebSocket(WebSocketCloseStatus.NormalClosure, CancellationToken.None);
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private void CheckConnection()
|
private void CheckConnection()
|
||||||
{
|
{
|
||||||
if (!_hasConnection)
|
if (!_hasConnection)
|
||||||
@@ -125,6 +161,53 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeWebsocketClient(TimeSpan delay)
|
||||||
|
{
|
||||||
|
if (_serviceEndpoint.Authorization != null &&
|
||||||
|
_serviceEndpoint.Authorization.Parameters.TryGetValue(EndpointAuthorizationParameters.AccessToken, out var accessToken) &&
|
||||||
|
!string.IsNullOrEmpty(accessToken))
|
||||||
|
{
|
||||||
|
if (_serviceEndpoint.Data.TryGetValue("FeedStreamUrl", out var feedStreamUrl) && !string.IsNullOrEmpty(feedStreamUrl))
|
||||||
|
{
|
||||||
|
// let's ensure we use the right scheme
|
||||||
|
feedStreamUrl = feedStreamUrl.Replace("https://", "wss://").Replace("http://", "ws://");
|
||||||
|
Trace.Info($"Creating websocket client ..." + feedStreamUrl);
|
||||||
|
this._websocketClient = new ClientWebSocket();
|
||||||
|
this._websocketClient.Options.SetRequestHeader("Authorization", $"Bearer {accessToken}");
|
||||||
|
var userAgentValues = new List<ProductInfoHeaderValue>();
|
||||||
|
userAgentValues.AddRange(UserAgentUtility.GetDefaultRestUserAgent());
|
||||||
|
userAgentValues.AddRange(HostContext.UserAgents);
|
||||||
|
this._websocketClient.Options.SetRequestHeader("User-Agent", string.Join(" ", userAgentValues.Select(x=>x.ToString())));
|
||||||
|
|
||||||
|
this._websocketConnectTask = ConnectWebSocketClient(feedStreamUrl, delay);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"No FeedStreamUrl found, so we will use Rest API calls for sending feed data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"No access token from the service endpoint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConnectWebSocketClient(string feedStreamUrl, TimeSpan delay)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Trace.Info($"Attempting to start websocket client with delay {delay}.");
|
||||||
|
await Task.Delay(delay);
|
||||||
|
await this._websocketClient.ConnectAsync(new Uri(feedStreamUrl), default(CancellationToken));
|
||||||
|
Trace.Info($"Successfully started websocket client.");
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Info("Exception caught during websocket client connect, fallback of HTTP would be used now instead of websocket.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------
|
//-----------------------------------------------------------------
|
||||||
// Feedback: WebConsole, TimelineRecords and Logs
|
// Feedback: WebConsole, TimelineRecords and Logs
|
||||||
//-----------------------------------------------------------------
|
//-----------------------------------------------------------------
|
||||||
@@ -135,16 +218,86 @@ namespace GitHub.Runner.Common
|
|||||||
return _taskClient.AppendLogContentAsync(scopeIdentifier, hubName, planId, logId, uploadStream, cancellationToken: cancellationToken);
|
return _taskClient.AppendLogContentAsync(scopeIdentifier, hubName, planId, logId, uploadStream, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, CancellationToken cancellationToken)
|
public async Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, long? startLine, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.AppendTimelineRecordFeedAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, stepId, lines, cancellationToken: cancellationToken);
|
var pushedLinesViaWebsocket = false;
|
||||||
|
if (_websocketConnectTask != null)
|
||||||
|
{
|
||||||
|
await _websocketConnectTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "_websocketClient != null" implies either: We have a successful connection OR we have to attempt sending again and then reconnect
|
||||||
|
// ...in other words, if websocket client is null, we will skip sending to websocket and just use rest api calls to send data
|
||||||
|
if (_websocketClient != null)
|
||||||
|
{
|
||||||
|
var linesWrapper = startLine.HasValue? new TimelineRecordFeedLinesWrapper(stepId, lines, startLine.Value): new TimelineRecordFeedLinesWrapper(stepId, lines);
|
||||||
|
var jsonData = StringUtil.ConvertToJson(linesWrapper);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
totalBatchedLinesAttemptedByWebsocket++;
|
||||||
|
var jsonDataBytes = Encoding.UTF8.GetBytes(jsonData);
|
||||||
|
// break the message into chunks of 1024 bytes
|
||||||
|
for (var i = 0; i < jsonDataBytes.Length; i += 1 * 1024)
|
||||||
|
{
|
||||||
|
var lastChunk = i + (1 * 1024) >= jsonDataBytes.Length;
|
||||||
|
var chunk = new ArraySegment<byte>(jsonDataBytes, i, Math.Min(1 * 1024, jsonDataBytes.Length - i));
|
||||||
|
await _websocketClient.SendAsync(chunk, WebSocketMessageType.Text, endOfMessage:lastChunk, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
pushedLinesViaWebsocket = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failedAttemptsToPostBatchedLinesByWebsocket++;
|
||||||
|
Trace.Info($"Caught exception during append web console line to websocket, let's fallback to sending via non-websocket call (total calls: {totalBatchedLinesAttemptedByWebsocket}, failed calls: {failedAttemptsToPostBatchedLinesByWebsocket}, websocket state: {this._websocketClient?.State}).");
|
||||||
|
Trace.Error(ex);
|
||||||
|
if (totalBatchedLinesAttemptedByWebsocket > _minWebsocketBatchedLinesCountToConsider)
|
||||||
|
{
|
||||||
|
// let's consider failure percentage
|
||||||
|
if (failedAttemptsToPostBatchedLinesByWebsocket * 100 / totalBatchedLinesAttemptedByWebsocket > _minWebsocketFailurePercentageAllowed)
|
||||||
|
{
|
||||||
|
Trace.Info($"Exhausted websocket allowed retries, we will not attempt websocket connection for this job to post lines again.");
|
||||||
|
CloseWebSocket(WebSocketCloseStatus.InternalServerError, cancellationToken);
|
||||||
|
|
||||||
|
// By setting it to null, we will ensure that we never try websocket path again for this job
|
||||||
|
_websocketClient = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_websocketClient != null)
|
||||||
|
{
|
||||||
|
var delay = BackoffTimerHelper.GetRandomBackoff(_minDelayForWebsocketReconnect, _maxDelayForWebsocketReconnect);
|
||||||
|
Trace.Info($"Websocket is not open, let's attempt to connect back again with random backoff {delay} ms (total calls: {totalBatchedLinesAttemptedByWebsocket}, failed calls: {failedAttemptsToPostBatchedLinesByWebsocket}).");
|
||||||
|
InitializeWebsocketClient(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pushedLinesViaWebsocket)
|
||||||
|
{
|
||||||
|
if (startLine.HasValue)
|
||||||
|
{
|
||||||
|
await _taskClient.AppendTimelineRecordFeedAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, stepId, lines, startLine.Value, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _taskClient.AppendTimelineRecordFeedAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, stepId, lines, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, long startLine, CancellationToken cancellationToken)
|
private void CloseWebSocket(WebSocketCloseStatus closeStatus, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
try
|
||||||
return _taskClient.AppendTimelineRecordFeedAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, stepId, lines, startLine, cancellationToken: cancellationToken);
|
{
|
||||||
|
_websocketClient?.CloseOutputAsync(closeStatus, "Closing websocket", cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception websocketEx)
|
||||||
|
{
|
||||||
|
// In some cases this might be okay since the websocket might be open yet, so just close and don't trace exceptions
|
||||||
|
Trace.Info($"Failed to close websocket gracefully {websocketEx.GetType().Name}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, string type, string name, Stream uploadStream, CancellationToken cancellationToken)
|
public Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, string type, string name, Stream uploadStream, CancellationToken cancellationToken)
|
||||||
@@ -186,10 +339,10 @@ namespace GitHub.Runner.Common
|
|||||||
//-----------------------------------------------------------------
|
//-----------------------------------------------------------------
|
||||||
// Action download info
|
// Action download info
|
||||||
//-----------------------------------------------------------------
|
//-----------------------------------------------------------------
|
||||||
public Task<ActionDownloadInfoCollection> ResolveActionDownloadInfoAsync(Guid scopeIdentifier, string hubName, Guid planId, ActionReferenceList actions, CancellationToken cancellationToken)
|
public Task<ActionDownloadInfoCollection> ResolveActionDownloadInfoAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return _taskClient.ResolveActionDownloadInfoAsync(scopeIdentifier, hubName, planId, actions, cancellationToken: cancellationToken);
|
return _taskClient.ResolveActionDownloadInfoAsync(scopeIdentifier, hubName, planId, jobId, actions, cancellationToken: cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
namespace GitHub.Runner.Common
|
||||||
{
|
{
|
||||||
@@ -72,10 +71,11 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
// Web console dequeue will start with process queue every 250ms for the first 60*4 times (~60 seconds).
|
// Web console dequeue will start with process queue every 250ms for the first 60*4 times (~60 seconds).
|
||||||
// Then the dequeue will happen every 500ms.
|
// Then the dequeue will happen every 500ms.
|
||||||
// In this way, customer still can get instance live console output on job start,
|
// In this way, customer still can get instance live console output on job start,
|
||||||
// at the same time we can cut the load to server after the build run for more than 60s
|
// at the same time we can cut the load to server after the build run for more than 60s
|
||||||
private int _webConsoleLineAggressiveDequeueCount = 0;
|
private int _webConsoleLineAggressiveDequeueCount = 0;
|
||||||
private const int _webConsoleLineAggressiveDequeueLimit = 4 * 60;
|
private const int _webConsoleLineAggressiveDequeueLimit = 4 * 60;
|
||||||
|
private const int _webConsoleLineQueueSizeLimit = 1024;
|
||||||
private bool _webConsoleLineAggressiveDequeue = true;
|
private bool _webConsoleLineAggressiveDequeue = true;
|
||||||
private bool _firstConsoleOutputs = true;
|
private bool _firstConsoleOutputs = true;
|
||||||
|
|
||||||
@@ -89,6 +89,10 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
|
|
||||||
|
var serviceEndPoint = jobRequest.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
_jobServer.InitializeWebsocketClient(serviceEndPoint);
|
||||||
|
|
||||||
if (_queueInProcess)
|
if (_queueInProcess)
|
||||||
{
|
{
|
||||||
Trace.Info("No-opt, all queue process tasks are running.");
|
Trace.Info("No-opt, all queue process tasks are running.");
|
||||||
@@ -156,13 +160,28 @@ namespace GitHub.Runner.Common
|
|||||||
await ProcessTimelinesUpdateQueueAsync(runOnce: true);
|
await ProcessTimelinesUpdateQueueAsync(runOnce: true);
|
||||||
Trace.Info("Timeline update queue drained.");
|
Trace.Info("Timeline update queue drained.");
|
||||||
|
|
||||||
|
Trace.Info($"Disposing job server ...");
|
||||||
|
await _jobServer.DisposeAsync();
|
||||||
|
|
||||||
Trace.Info("All queue process tasks have been stopped, and all queues are drained.");
|
Trace.Info("All queue process tasks have been stopped, and all queues are drained.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber)
|
public void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber)
|
||||||
{
|
{
|
||||||
Trace.Verbose("Enqueue web console line queue: {0}", line);
|
// We only process 500 lines of the queue everytime.
|
||||||
_webConsoleLineQueue.Enqueue(new ConsoleLineInfo(stepRecordId, line, lineNumber));
|
// If the queue is backing up due to slow Http request or flood of output from step,
|
||||||
|
// we will drop the output to avoid extra memory consumption from the runner since the live console feed is best effort.
|
||||||
|
if (!string.IsNullOrEmpty(line) && _webConsoleLineQueue.Count < _webConsoleLineQueueSizeLimit)
|
||||||
|
{
|
||||||
|
Trace.Verbose("Enqueue web console line queue: {0}", line);
|
||||||
|
if (line.Length > 1024)
|
||||||
|
{
|
||||||
|
Trace.Verbose("Web console line is more than 1024 chars, truncate to first 1024 chars");
|
||||||
|
line = $"{line.Substring(0, 1024)}...";
|
||||||
|
}
|
||||||
|
|
||||||
|
_webConsoleLineQueue.Enqueue(new ConsoleLineInfo(stepRecordId, line, lineNumber));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource)
|
public void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource)
|
||||||
@@ -230,12 +249,6 @@ namespace GitHub.Runner.Common
|
|||||||
stepRecordIds.Add(lineInfo.StepRecordId);
|
stepRecordIds.Add(lineInfo.StepRecordId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(lineInfo.Line) && lineInfo.Line.Length > 1024)
|
|
||||||
{
|
|
||||||
Trace.Verbose("Web console line is more than 1024 chars, truncate to first 1024 chars");
|
|
||||||
lineInfo.Line = $"{lineInfo.Line.Substring(0, 1024)}...";
|
|
||||||
}
|
|
||||||
|
|
||||||
stepsConsoleLines[lineInfo.StepRecordId].Add(new TimelineRecordLogLine(lineInfo.Line, lineInfo.LineNumber));
|
stepsConsoleLines[lineInfo.StepRecordId].Add(new TimelineRecordLogLine(lineInfo.Line, lineInfo.LineNumber));
|
||||||
linesCounter++;
|
linesCounter++;
|
||||||
|
|
||||||
@@ -286,15 +299,7 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// we will not requeue failed batch, since the web console lines are time sensitive.
|
await _jobServer.AppendTimelineRecordFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch.Select(logLine => logLine.Line).ToList(), batch[0].LineNumber, default(CancellationToken));
|
||||||
if (batch[0].LineNumber.HasValue)
|
|
||||||
{
|
|
||||||
await _jobServer.AppendTimelineRecordFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch.Select(logLine => logLine.Line).ToList(), batch[0].LineNumber.Value, default(CancellationToken));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _jobServer.AppendTimelineRecordFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch.Select(logLine => logLine.Line).ToList(), default(CancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_firstConsoleOutputs)
|
if (_firstConsoleOutputs)
|
||||||
{
|
{
|
||||||
@@ -483,8 +488,8 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
if (runOnce)
|
if (runOnce)
|
||||||
{
|
{
|
||||||
// continue process timeline records update,
|
// continue process timeline records update,
|
||||||
// we might have more records need update,
|
// we might have more records need update,
|
||||||
// since we just create a new sub-timeline
|
// since we just create a new sub-timeline
|
||||||
if (pendingSubtimelineUpdate)
|
if (pendingSubtimelineUpdate)
|
||||||
{
|
{
|
||||||
|
|||||||
16
src/Runner.Common/Util/NodeUtil.cs
Normal file
16
src/Runner.Common/Util/NodeUtil.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common.Util
|
||||||
|
{
|
||||||
|
public static class NodeUtil
|
||||||
|
{
|
||||||
|
private const string _defaultNodeVersion = "node16";
|
||||||
|
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] {"node12", "node16"});
|
||||||
|
public static string GetInternalNodeVersion()
|
||||||
|
{
|
||||||
|
var forcedNodeVersion = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion);
|
||||||
|
return !string.IsNullOrEmpty(forcedNodeVersion) && BuiltInNodeVersions.Contains(forcedNodeVersion) ? forcedNodeVersion : _defaultNodeVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ using System.Net.NetworkInformation;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
|
|
||||||
@@ -314,12 +315,12 @@ namespace GitHub.Runner.Listener.Check
|
|||||||
});
|
});
|
||||||
|
|
||||||
var downloadCertScript = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Bin), "checkScripts", "downloadCert");
|
var downloadCertScript = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Bin), "checkScripts", "downloadCert");
|
||||||
var node12 = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), "node12", "bin", $"node{IOUtil.ExeExtension}");
|
var node = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}");
|
||||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run '{node12} \"{downloadCertScript}\"' ");
|
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run '{node} \"{downloadCertScript}\"' ");
|
||||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {StringUtil.ConvertToJson(env)}");
|
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {StringUtil.ConvertToJson(env)}");
|
||||||
await processInvoker.ExecuteAsync(
|
await processInvoker.ExecuteAsync(
|
||||||
hostContext.GetDirectory(WellKnownDirectory.Root),
|
hostContext.GetDirectory(WellKnownDirectory.Root),
|
||||||
node12,
|
node,
|
||||||
$"\"{downloadCertScript}\"",
|
$"\"{downloadCertScript}\"",
|
||||||
env,
|
env,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Net;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener.Check
|
namespace GitHub.Runner.Listener.Check
|
||||||
@@ -144,12 +145,12 @@ namespace GitHub.Runner.Listener.Check
|
|||||||
});
|
});
|
||||||
|
|
||||||
var makeWebRequestScript = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "checkScripts", "makeWebRequest.js");
|
var makeWebRequestScript = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "checkScripts", "makeWebRequest.js");
|
||||||
var node12 = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "node12", "bin", $"node{IOUtil.ExeExtension}");
|
var node = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}");
|
||||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run '{node12} \"{makeWebRequestScript}\"' ");
|
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run '{node} \"{makeWebRequestScript}\"' ");
|
||||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {StringUtil.ConvertToJson(env)}");
|
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {StringUtil.ConvertToJson(env)}");
|
||||||
await processInvoker.ExecuteAsync(
|
await processInvoker.ExecuteAsync(
|
||||||
HostContext.GetDirectory(WellKnownDirectory.Root),
|
HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||||
node12,
|
node,
|
||||||
$"\"{makeWebRequestScript}\"",
|
$"\"{makeWebRequestScript}\"",
|
||||||
env,
|
env,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -17,42 +17,57 @@ namespace GitHub.Runner.Listener
|
|||||||
private readonly IPromptManager _promptManager;
|
private readonly IPromptManager _promptManager;
|
||||||
private readonly Tracing _trace;
|
private readonly Tracing _trace;
|
||||||
|
|
||||||
private readonly string[] validCommands =
|
// Valid flags for all commands
|
||||||
|
private readonly string[] genericOptions =
|
||||||
{
|
{
|
||||||
Constants.Runner.CommandLine.Commands.Configure,
|
|
||||||
Constants.Runner.CommandLine.Commands.Remove,
|
|
||||||
Constants.Runner.CommandLine.Commands.Run,
|
|
||||||
Constants.Runner.CommandLine.Commands.Warmup,
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly string[] validFlags =
|
|
||||||
{
|
|
||||||
Constants.Runner.CommandLine.Flags.Check,
|
|
||||||
Constants.Runner.CommandLine.Flags.Commit,
|
|
||||||
Constants.Runner.CommandLine.Flags.Ephemeral,
|
|
||||||
Constants.Runner.CommandLine.Flags.Help,
|
Constants.Runner.CommandLine.Flags.Help,
|
||||||
Constants.Runner.CommandLine.Flags.Once,
|
Constants.Runner.CommandLine.Flags.Version,
|
||||||
Constants.Runner.CommandLine.Flags.Replace,
|
Constants.Runner.CommandLine.Flags.Commit,
|
||||||
Constants.Runner.CommandLine.Flags.RunAsService,
|
Constants.Runner.CommandLine.Flags.Check
|
||||||
Constants.Runner.CommandLine.Flags.Unattended,
|
|
||||||
Constants.Runner.CommandLine.Flags.Version
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly string[] validArgs =
|
// Valid flags and args for specific command - key: command, value: array of valid flags and args
|
||||||
|
private readonly Dictionary<string, string[]> validOptions = new Dictionary<string, string[]>
|
||||||
{
|
{
|
||||||
Constants.Runner.CommandLine.Args.Auth,
|
// Valid configure flags and args
|
||||||
Constants.Runner.CommandLine.Args.Labels,
|
[Constants.Runner.CommandLine.Commands.Configure] =
|
||||||
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
|
new string[]
|
||||||
Constants.Runner.CommandLine.Args.Name,
|
{
|
||||||
Constants.Runner.CommandLine.Args.PAT,
|
Constants.Runner.CommandLine.Flags.DisableUpdate,
|
||||||
Constants.Runner.CommandLine.Args.RunnerGroup,
|
Constants.Runner.CommandLine.Flags.Ephemeral,
|
||||||
Constants.Runner.CommandLine.Args.StartupType,
|
Constants.Runner.CommandLine.Flags.Replace,
|
||||||
Constants.Runner.CommandLine.Args.Token,
|
Constants.Runner.CommandLine.Flags.RunAsService,
|
||||||
Constants.Runner.CommandLine.Args.Url,
|
Constants.Runner.CommandLine.Flags.Unattended,
|
||||||
Constants.Runner.CommandLine.Args.UserName,
|
Constants.Runner.CommandLine.Args.Auth,
|
||||||
Constants.Runner.CommandLine.Args.WindowsLogonAccount,
|
Constants.Runner.CommandLine.Args.Labels,
|
||||||
Constants.Runner.CommandLine.Args.WindowsLogonPassword,
|
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
|
||||||
Constants.Runner.CommandLine.Args.Work
|
Constants.Runner.CommandLine.Args.Name,
|
||||||
|
Constants.Runner.CommandLine.Args.PAT,
|
||||||
|
Constants.Runner.CommandLine.Args.RunnerGroup,
|
||||||
|
Constants.Runner.CommandLine.Args.Token,
|
||||||
|
Constants.Runner.CommandLine.Args.Url,
|
||||||
|
Constants.Runner.CommandLine.Args.UserName,
|
||||||
|
Constants.Runner.CommandLine.Args.WindowsLogonAccount,
|
||||||
|
Constants.Runner.CommandLine.Args.WindowsLogonPassword,
|
||||||
|
Constants.Runner.CommandLine.Args.Work
|
||||||
|
},
|
||||||
|
// Valid remove flags and args
|
||||||
|
[Constants.Runner.CommandLine.Commands.Remove] =
|
||||||
|
new string[]
|
||||||
|
{
|
||||||
|
Constants.Runner.CommandLine.Args.Token,
|
||||||
|
Constants.Runner.CommandLine.Args.PAT
|
||||||
|
},
|
||||||
|
// Valid run flags and args
|
||||||
|
[Constants.Runner.CommandLine.Commands.Run] =
|
||||||
|
new string[]
|
||||||
|
{
|
||||||
|
Constants.Runner.CommandLine.Flags.Once,
|
||||||
|
Constants.Runner.CommandLine.Args.StartupType
|
||||||
|
},
|
||||||
|
// valid warmup flags and args
|
||||||
|
[Constants.Runner.CommandLine.Commands.Warmup] =
|
||||||
|
new string[] { }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Commands.
|
// Commands.
|
||||||
@@ -68,6 +83,7 @@ namespace GitHub.Runner.Listener
|
|||||||
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
|
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
|
||||||
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
|
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
|
||||||
public bool Ephemeral => TestFlag(Constants.Runner.CommandLine.Flags.Ephemeral);
|
public bool Ephemeral => TestFlag(Constants.Runner.CommandLine.Flags.Ephemeral);
|
||||||
|
public bool DisableUpdate => TestFlag(Constants.Runner.CommandLine.Flags.DisableUpdate);
|
||||||
|
|
||||||
// Keep this around since customers still relies on it
|
// Keep this around since customers still relies on it
|
||||||
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
|
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
|
||||||
@@ -124,17 +140,48 @@ namespace GitHub.Runner.Listener
|
|||||||
List<string> unknowns = new List<string>();
|
List<string> unknowns = new List<string>();
|
||||||
|
|
||||||
// detect unknown commands
|
// detect unknown commands
|
||||||
unknowns.AddRange(_parser.Commands.Where(x => !validCommands.Contains(x, StringComparer.OrdinalIgnoreCase)));
|
unknowns.AddRange(_parser.Commands.Where(x => !validOptions.Keys.Contains(x, StringComparer.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
// detect unknown flags
|
if (unknowns.Count == 0)
|
||||||
unknowns.AddRange(_parser.Flags.Where(x => !validFlags.Contains(x, StringComparer.OrdinalIgnoreCase)));
|
{
|
||||||
|
// detect unknown flags and args for valid commands
|
||||||
// detect unknown args
|
foreach (var command in _parser.Commands)
|
||||||
unknowns.AddRange(_parser.Args.Keys.Where(x => !validArgs.Contains(x, StringComparer.OrdinalIgnoreCase)));
|
{
|
||||||
|
if (validOptions.TryGetValue(command, out string[] options))
|
||||||
|
{
|
||||||
|
unknowns.AddRange(_parser.Flags.Where(x => !options.Contains(x, StringComparer.OrdinalIgnoreCase) && !genericOptions.Contains(x, StringComparer.OrdinalIgnoreCase)));
|
||||||
|
unknowns.AddRange(_parser.Args.Keys.Where(x => !options.Contains(x, StringComparer.OrdinalIgnoreCase)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return unknowns;
|
return unknowns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetCommandName()
|
||||||
|
{
|
||||||
|
string command = string.Empty;
|
||||||
|
|
||||||
|
if (Configure)
|
||||||
|
{
|
||||||
|
command = Constants.Runner.CommandLine.Commands.Configure;
|
||||||
|
}
|
||||||
|
else if (Remove)
|
||||||
|
{
|
||||||
|
command = Constants.Runner.CommandLine.Commands.Remove;
|
||||||
|
}
|
||||||
|
else if (Run)
|
||||||
|
{
|
||||||
|
command = Constants.Runner.CommandLine.Commands.Run;
|
||||||
|
}
|
||||||
|
else if (Warmup)
|
||||||
|
{
|
||||||
|
command = Constants.Runner.CommandLine.Commands.Warmup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Interactive flags.
|
// Interactive flags.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
Trace.Info(nameof(LoadSettings));
|
Trace.Info(nameof(LoadSettings));
|
||||||
if (!IsConfigured())
|
if (!IsConfigured())
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Not configured. Run config.(sh/cmd) to configure the runner.");
|
throw new NonRetryableException("Not configured. Run config.(sh/cmd) to configure the runner.");
|
||||||
}
|
}
|
||||||
|
|
||||||
RunnerSettings settings = _store.GetSettings();
|
RunnerSettings settings = _store.GetSettings();
|
||||||
@@ -196,6 +196,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
TaskAgent agent;
|
TaskAgent agent;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
runnerSettings.DisableUpdate = command.DisableUpdate;
|
||||||
runnerSettings.Ephemeral = command.Ephemeral;
|
runnerSettings.Ephemeral = command.Ephemeral;
|
||||||
runnerSettings.AgentName = command.GetRunnerName();
|
runnerSettings.AgentName = command.GetRunnerName();
|
||||||
|
|
||||||
@@ -213,11 +214,22 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
if (command.GetReplace())
|
if (command.GetReplace())
|
||||||
{
|
{
|
||||||
// Update existing agent with new PublicKey, agent version.
|
// Update existing agent with new PublicKey, agent version.
|
||||||
agent = UpdateExistingAgent(agent, publicKey, userLabels, runnerSettings.Ephemeral);
|
agent = UpdateExistingAgent(agent, publicKey, userLabels, runnerSettings.Ephemeral, command.DisableUpdate);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
agent = await _runnerServer.ReplaceAgentAsync(runnerSettings.PoolId, agent);
|
agent = await _runnerServer.ReplaceAgentAsync(runnerSettings.PoolId, agent);
|
||||||
|
if (command.DisableUpdate &&
|
||||||
|
command.DisableUpdate != agent.DisableUpdate)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("The GitHub server does not support configuring a self-hosted runner with 'DisableUpdate' flag.");
|
||||||
|
}
|
||||||
|
if (command.Ephemeral &&
|
||||||
|
command.Ephemeral != agent.Ephemeral)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("The GitHub server does not support configuring a self-hosted runner with 'Ephemeral' flag.");
|
||||||
|
}
|
||||||
|
|
||||||
_term.WriteSuccessMessage("Successfully replaced the runner");
|
_term.WriteSuccessMessage("Successfully replaced the runner");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -236,11 +248,22 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Create a new agent.
|
// Create a new agent.
|
||||||
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels, runnerSettings.Ephemeral);
|
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels, runnerSettings.Ephemeral, command.DisableUpdate);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
agent = await _runnerServer.AddAgentAsync(runnerSettings.PoolId, agent);
|
agent = await _runnerServer.AddAgentAsync(runnerSettings.PoolId, agent);
|
||||||
|
if (command.DisableUpdate &&
|
||||||
|
command.DisableUpdate != agent.DisableUpdate)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("The GitHub server does not support configuring a self-hosted runner with 'DisableUpdate' flag.");
|
||||||
|
}
|
||||||
|
if (command.Ephemeral &&
|
||||||
|
command.Ephemeral != agent.Ephemeral)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("The GitHub server does not support configuring a self-hosted runner with 'Ephemeral' flag.");
|
||||||
|
}
|
||||||
|
|
||||||
_term.WriteSuccessMessage("Runner successfully added");
|
_term.WriteSuccessMessage("Runner successfully added");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -466,7 +489,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral)
|
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral, bool disableUpdate)
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(agent, nameof(agent));
|
ArgUtil.NotNull(agent, nameof(agent));
|
||||||
agent.Authorization = new TaskAgentAuthorization
|
agent.Authorization = new TaskAgentAuthorization
|
||||||
@@ -478,6 +501,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
agent.Version = BuildConstants.RunnerPackage.Version;
|
agent.Version = BuildConstants.RunnerPackage.Version;
|
||||||
agent.OSDescription = RuntimeInformation.OSDescription;
|
agent.OSDescription = RuntimeInformation.OSDescription;
|
||||||
agent.Ephemeral = ephemeral;
|
agent.Ephemeral = ephemeral;
|
||||||
|
agent.DisableUpdate = disableUpdate;
|
||||||
agent.MaxParallelism = 1;
|
agent.MaxParallelism = 1;
|
||||||
|
|
||||||
agent.Labels.Clear();
|
agent.Labels.Clear();
|
||||||
@@ -494,7 +518,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
return agent;
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral)
|
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral, bool disableUpdate)
|
||||||
{
|
{
|
||||||
TaskAgent agent = new TaskAgent(agentName)
|
TaskAgent agent = new TaskAgent(agentName)
|
||||||
{
|
{
|
||||||
@@ -506,6 +530,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
Version = BuildConstants.RunnerPackage.Version,
|
Version = BuildConstants.RunnerPackage.Version,
|
||||||
OSDescription = RuntimeInformation.OSDescription,
|
OSDescription = RuntimeInformation.OSDescription,
|
||||||
Ephemeral = ephemeral,
|
Ephemeral = ephemeral,
|
||||||
|
DisableUpdate = disableUpdate
|
||||||
};
|
};
|
||||||
|
|
||||||
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
|
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
|
||||||
@@ -588,32 +613,50 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
throw new ArgumentException($"'{githubUrl}' should point to an org or repository.");
|
throw new ArgumentException($"'{githubUrl}' should point to an org or repository.");
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
int retryCount = 0;
|
||||||
using (var httpClient = new HttpClient(httpClientHandler))
|
while(retryCount < 3)
|
||||||
{
|
{
|
||||||
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"github:{githubToken}"));
|
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||||
HostContext.SecretMasker.AddValue(base64EncodingToken);
|
using (var httpClient = new HttpClient(httpClientHandler))
|
||||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", base64EncodingToken);
|
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
|
||||||
httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github.v3+json");
|
|
||||||
|
|
||||||
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(string.Empty));
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
Trace.Info($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'");
|
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"github:{githubToken}"));
|
||||||
var jsonResponse = await response.Content.ReadAsStringAsync();
|
HostContext.SecretMasker.AddValue(base64EncodingToken);
|
||||||
return StringUtil.ConvertFromJson<GitHubRunnerRegisterToken>(jsonResponse);
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", base64EncodingToken);
|
||||||
}
|
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||||
else
|
httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github.v3+json");
|
||||||
{
|
|
||||||
_term.WriteError($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'");
|
var responseStatus = System.Net.HttpStatusCode.OK;
|
||||||
var errorResponse = await response.Content.ReadAsStringAsync();
|
try
|
||||||
_term.WriteError(errorResponse);
|
{
|
||||||
response.EnsureSuccessStatusCode();
|
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(string.Empty));
|
||||||
return null;
|
responseStatus = response.StatusCode;
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
Trace.Info($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'");
|
||||||
|
var jsonResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
return StringUtil.ConvertFromJson<GitHubRunnerRegisterToken>(jsonResponse);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_term.WriteError($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'");
|
||||||
|
var errorResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
_term.WriteError(errorResponse);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception ex) when (retryCount < 2 && responseStatus != System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
retryCount++;
|
||||||
|
Trace.Error($"Failed to get JIT runner token -- Atempt: {retryCount}");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5));
|
||||||
|
Trace.Info($"Retrying in {backOff.Seconds} seconds");
|
||||||
|
await Task.Delay(backOff);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<GitHubAuthResult> GetTenantCredential(string githubUrl, string githubToken, string runnerEvent)
|
private async Task<GitHubAuthResult> GetTenantCredential(string githubUrl, string githubToken, string runnerEvent)
|
||||||
@@ -629,35 +672,53 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/actions/runner-registration";
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/actions/runner-registration";
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
int retryCount = 0;
|
||||||
using (var httpClient = new HttpClient(httpClientHandler))
|
while (retryCount < 3)
|
||||||
{
|
{
|
||||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
|
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
using (var httpClient = new HttpClient(httpClientHandler))
|
||||||
|
|
||||||
var bodyObject = new Dictionary<string, string>()
|
|
||||||
{
|
{
|
||||||
{"url", githubUrl},
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
|
||||||
{"runner_event", runnerEvent}
|
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||||
};
|
|
||||||
|
|
||||||
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"));
|
var bodyObject = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"url", githubUrl},
|
||||||
|
{"runner_event", runnerEvent}
|
||||||
|
};
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
var responseStatus = System.Net.HttpStatusCode.OK;
|
||||||
{
|
try
|
||||||
Trace.Info($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'");
|
{
|
||||||
var jsonResponse = await response.Content.ReadAsStringAsync();
|
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"));
|
||||||
return StringUtil.ConvertFromJson<GitHubAuthResult>(jsonResponse);
|
responseStatus = response.StatusCode;
|
||||||
}
|
|
||||||
else
|
if(response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
_term.WriteError($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'");
|
Trace.Info($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'");
|
||||||
var errorResponse = await response.Content.ReadAsStringAsync();
|
var jsonResponse = await response.Content.ReadAsStringAsync();
|
||||||
_term.WriteError(errorResponse);
|
return StringUtil.ConvertFromJson<GitHubAuthResult>(jsonResponse);
|
||||||
response.EnsureSuccessStatusCode();
|
}
|
||||||
return null;
|
else
|
||||||
|
{
|
||||||
|
_term.WriteError($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'");
|
||||||
|
var errorResponse = await response.Content.ReadAsStringAsync();
|
||||||
|
_term.WriteError(errorResponse);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception ex) when (retryCount < 2 && responseStatus != System.Net.HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
retryCount++;
|
||||||
|
Trace.Error($"Failed to get tenant credentials -- Atempt: {retryCount}");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5));
|
||||||
|
Trace.Info($"Retrying in {backOff.Seconds} seconds");
|
||||||
|
await Task.Delay(backOff);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
LocalGroupInfo groupInfo = new LocalGroupInfo();
|
LocalGroupInfo groupInfo = new LocalGroupInfo();
|
||||||
groupInfo.Name = groupName;
|
groupInfo.Name = groupName;
|
||||||
groupInfo.Comment = StringUtil.Format("Built-in group used by Team Foundation Server.");
|
groupInfo.Comment = StringUtil.Format("Built-in group used by GitHub Actions Runner.");
|
||||||
|
|
||||||
int returnCode = NetLocalGroupAdd(null, // computer name
|
int returnCode = NetLocalGroupAdd(null, // computer name
|
||||||
1, // 1 means include comment
|
1, // 1 means include comment
|
||||||
|
|||||||
@@ -48,13 +48,12 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
string repoOrOrgName = regex.Replace(settings.RepoOrOrgName, "-");
|
string repoOrOrgName = regex.Replace(settings.RepoOrOrgName, "-");
|
||||||
|
|
||||||
serviceName = StringUtil.Format(serviceNamePattern, repoOrOrgName, settings.AgentName);
|
serviceName = StringUtil.Format(serviceNamePattern, repoOrOrgName, settings.AgentName);
|
||||||
|
if (serviceName.Length > MaxServiceNameLength)
|
||||||
if (serviceName.Length > 80)
|
|
||||||
{
|
{
|
||||||
Trace.Verbose($"Calculated service name is too long (> 80 chars). Trying again by calculating a shorter name.");
|
Trace.Verbose($"Calculated service name is too long (> {MaxServiceNameLength} chars). Trying again by calculating a shorter name.");
|
||||||
|
// Add 5 to add -xxxx random number on the end
|
||||||
int exceededCharLength = serviceName.Length - 80;
|
int exceededCharLength = serviceName.Length - MaxServiceNameLength + 5;
|
||||||
string repoOrOrgNameSubstring = StringUtil.SubstringPrefix(repoOrOrgName, 45);
|
string repoOrOrgNameSubstring = StringUtil.SubstringPrefix(repoOrOrgName, MaxRepoOrgCharacters);
|
||||||
|
|
||||||
exceededCharLength -= repoOrOrgName.Length - repoOrOrgNameSubstring.Length;
|
exceededCharLength -= repoOrOrgName.Length - repoOrOrgNameSubstring.Length;
|
||||||
|
|
||||||
@@ -66,6 +65,10 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
runnerNameSubstring = StringUtil.SubstringPrefix(settings.AgentName, settings.AgentName.Length - exceededCharLength);
|
runnerNameSubstring = StringUtil.SubstringPrefix(settings.AgentName, settings.AgentName.Length - exceededCharLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lets add a suffix with a random number to reduce the chance of collisions between runner names once we truncate
|
||||||
|
var random = new Random();
|
||||||
|
var num = random.Next(1000, 9999).ToString();
|
||||||
|
runnerNameSubstring +=$"-{num}";
|
||||||
serviceName = StringUtil.Format(serviceNamePattern, repoOrOrgNameSubstring, runnerNameSubstring);
|
serviceName = StringUtil.Format(serviceNamePattern, repoOrOrgNameSubstring, runnerNameSubstring);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,5 +76,12 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
Trace.Info($"Service name '{serviceName}' display name '{serviceDisplayName}' will be used for service configuration.");
|
Trace.Info($"Service name '{serviceName}' display name '{serviceDisplayName}' will be used for service configuration.");
|
||||||
}
|
}
|
||||||
|
#if (OS_LINUX || OS_OSX)
|
||||||
|
const int MaxServiceNameLength = 150;
|
||||||
|
const int MaxRepoOrgCharacters = 70;
|
||||||
|
#elif OS_WINDOWS
|
||||||
|
const int MaxServiceNameLength = 80;
|
||||||
|
const int MaxRepoOrgCharacters = 45;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ using System;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
|
||||||
using System.Linq;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using GitHub.Services.WebApi;
|
||||||
using GitHub.Services.WebApi.Jwt;
|
using GitHub.Services.WebApi.Jwt;
|
||||||
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener
|
namespace GitHub.Runner.Listener
|
||||||
{
|
{
|
||||||
@@ -34,6 +36,7 @@ namespace GitHub.Runner.Listener
|
|||||||
// and the server will not send another job while this one is still running.
|
// and the server will not send another job while this one is still running.
|
||||||
public sealed class JobDispatcher : RunnerService, IJobDispatcher
|
public sealed class JobDispatcher : RunnerService, IJobDispatcher
|
||||||
{
|
{
|
||||||
|
private static Regex _invalidJsonRegex = new Regex(@"invalid\ Json\ at\ position\ '(\d+)':", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
private readonly Lazy<Dictionary<long, TaskResult>> _localRunJobResult = new Lazy<Dictionary<long, TaskResult>>();
|
private readonly Lazy<Dictionary<long, TaskResult>> _localRunJobResult = new Lazy<Dictionary<long, TaskResult>>();
|
||||||
private int _poolId;
|
private int _poolId;
|
||||||
|
|
||||||
@@ -282,7 +285,7 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
// at this point, the job execution might encounter some dead lock and even not able to be cancelled.
|
// 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.
|
// 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.");
|
throw new InvalidOperationException($"Job dispatch process for {jobDispatch.JobId} has encountered unexpected error, the dispatch task is not able to be cancelled within 45 seconds.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -360,7 +363,7 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Info($"Start renew job request {requestId} for job {message.JobId}.");
|
Trace.Info($"Start renew job request {requestId} for job {message.JobId}.");
|
||||||
Task renewJobRequest = RenewJobRequestAsync(_poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
Task renewJobRequest = RenewJobRequestAsync(_poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
||||||
|
|
||||||
// wait till first renew succeed or job request is canceled
|
// wait till first renew succeed or job request is cancelled
|
||||||
// not even start worker if the first renew fail
|
// not even start worker if the first renew fail
|
||||||
await Task.WhenAny(firstJobRequestRenewed.Task, renewJobRequest, Task.Delay(-1, jobRequestCancellationToken));
|
await Task.WhenAny(firstJobRequestRenewed.Task, renewJobRequest, Task.Delay(-1, jobRequestCancellationToken));
|
||||||
|
|
||||||
@@ -701,7 +704,7 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
// OperationCanceledException may caused by http timeout or _lockRenewalTokenSource.Cance();
|
// OperationCanceledException may caused by http timeout or _lockRenewalTokenSource.Cance();
|
||||||
// Stop renew only on cancellation token fired.
|
// Stop renew only on cancellation token fired.
|
||||||
Trace.Info($"job renew has been canceled, stop renew job request {requestId}.");
|
Trace.Info($"job renew has been cancelled, stop renew job request {requestId}.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -759,7 +762,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
Trace.Info($"job renew has been canceled, stop renew job request {requestId}.");
|
Trace.Info($"job renew has been cancelled, stop renew job request {requestId}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -964,6 +967,30 @@ namespace GitHub.Runner.Listener
|
|||||||
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
||||||
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(errorMessage) &&
|
||||||
|
message.Variables.TryGetValue("DistributedTask.EnableRunnerIPCDebug", out var enableRunnerIPCDebug) &&
|
||||||
|
StringUtil.ConvertToBoolean(enableRunnerIPCDebug.Value))
|
||||||
|
{
|
||||||
|
// the trace should be best effort and not affect any job result
|
||||||
|
var match = _invalidJsonRegex.Match(errorMessage);
|
||||||
|
if (match.Success &&
|
||||||
|
match.Groups.Count == 2)
|
||||||
|
{
|
||||||
|
var jsonPosition = int.Parse(match.Groups[1].Value);
|
||||||
|
var serializedJobMessage = JsonUtility.ToString(message);
|
||||||
|
var originalJson = serializedJobMessage.Substring(jsonPosition - 10, 20);
|
||||||
|
errorMessage = $"Runner sent Json at position '{jsonPosition}': {originalJson} ({Convert.ToBase64String(Encoding.UTF8.GetBytes(originalJson))})\n{errorMessage}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error(ex);
|
||||||
|
errorMessage = $"Fail to check json IPC error: {ex.Message}\n{errorMessage}";
|
||||||
|
}
|
||||||
|
|
||||||
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = errorMessage };
|
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = errorMessage };
|
||||||
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
||||||
jobRecord.ErrorCount++;
|
jobRecord.ErrorCount++;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Listener.Configuration;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Security.Cryptography;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using GitHub.Services.OAuth;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Listener.Configuration;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using GitHub.Services.OAuth;
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener
|
namespace GitHub.Runner.Listener
|
||||||
{
|
{
|
||||||
@@ -33,6 +33,7 @@ namespace GitHub.Runner.Listener
|
|||||||
private IRunnerServer _runnerServer;
|
private IRunnerServer _runnerServer;
|
||||||
private TaskAgentSession _session;
|
private TaskAgentSession _session;
|
||||||
private TimeSpan _getNextMessageRetryInterval;
|
private TimeSpan _getNextMessageRetryInterval;
|
||||||
|
private bool _accessTokenRevoked = false;
|
||||||
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
|
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
|
||||||
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
|
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
|
||||||
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
|
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
|
||||||
@@ -111,6 +112,7 @@ namespace GitHub.Runner.Listener
|
|||||||
catch (TaskAgentAccessTokenExpiredException)
|
catch (TaskAgentAccessTokenExpiredException)
|
||||||
{
|
{
|
||||||
Trace.Info("Runner OAuth token has been revoked. Session creation failed.");
|
Trace.Info("Runner OAuth token has been revoked. Session creation failed.");
|
||||||
|
_accessTokenRevoked = true;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -154,9 +156,16 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
if (_session != null && _session.SessionId != Guid.Empty)
|
if (_session != null && _session.SessionId != Guid.Empty)
|
||||||
{
|
{
|
||||||
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
if (!_accessTokenRevoked)
|
||||||
{
|
{
|
||||||
await _runnerServer.DeleteAgentSessionAsync(_settings.PoolId, _session.SessionId, ts.Token);
|
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
||||||
|
{
|
||||||
|
await _runnerServer.DeleteAgentSessionAsync(_settings.PoolId, _session.SessionId, ts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Warning("Runner OAuth token has been revoked. Skip deleting session.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,6 +214,7 @@ namespace GitHub.Runner.Listener
|
|||||||
catch (TaskAgentAccessTokenExpiredException)
|
catch (TaskAgentAccessTokenExpiredException)
|
||||||
{
|
{
|
||||||
Trace.Info("Runner OAuth token has been revoked. Unable to pull message.");
|
Trace.Info("Runner OAuth token has been revoked. Unable to pull message.");
|
||||||
|
_accessTokenRevoked = true;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -95,7 +95,15 @@ namespace GitHub.Runner.Listener
|
|||||||
var unknownCommandlines = command.Validate();
|
var unknownCommandlines = command.Validate();
|
||||||
if (unknownCommandlines.Count > 0)
|
if (unknownCommandlines.Count > 0)
|
||||||
{
|
{
|
||||||
terminal.WriteError($"Unrecognized command-line input arguments: '{string.Join(", ", unknownCommandlines)}'. For usage refer to: .\\config.cmd --help or ./config.sh --help");
|
string commandName = command.GetCommandName();
|
||||||
|
if (string.IsNullOrEmpty(commandName))
|
||||||
|
{
|
||||||
|
terminal.WriteError($"This command does not recognize the command-line input arguments: '{string.Join(", ", unknownCommandlines)}'. For usage refer to: .\\config.cmd --help or ./config.sh --help");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
terminal.WriteError($"Unrecognized command-line input arguments for command {commandName}: '{string.Join(", ", unknownCommandlines)}'. For usage refer to: .\\config.cmd --help or ./config.sh --help");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defer to the Runner class to execute the command.
|
// Defer to the Runner class to execute the command.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
<NoWarn>NU1701;NU1603</NoWarn>
|
<NoWarn>NU1701;NU1603</NoWarn>
|
||||||
<Version>$(Version)</Version>
|
<Version>$(Version)</Version>
|
||||||
|
<PredefinedCulturesOnly>false</PredefinedCulturesOnly>
|
||||||
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
|
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using GitHub.Runner.Common;
|
|||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using GitHub.Runner.Listener.Check;
|
using GitHub.Runner.Listener.Check;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener
|
namespace GitHub.Runner.Listener
|
||||||
{
|
{
|
||||||
@@ -318,7 +319,7 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
IJobDispatcher jobDispatcher = null;
|
IJobDispatcher jobDispatcher = null;
|
||||||
CancellationTokenSource messageQueueLoopTokenSource = CancellationTokenSource.CreateLinkedTokenSource(HostContext.RunnerShutdownToken);
|
CancellationTokenSource messageQueueLoopTokenSource = CancellationTokenSource.CreateLinkedTokenSource(HostContext.RunnerShutdownToken);
|
||||||
|
|
||||||
// Should we try to cleanup ephemeral runners
|
// Should we try to cleanup ephemeral runners
|
||||||
bool runOnceJobCompleted = false;
|
bool runOnceJobCompleted = false;
|
||||||
try
|
try
|
||||||
@@ -407,8 +408,29 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
autoUpdateInProgress = true;
|
autoUpdateInProgress = true;
|
||||||
var runnerUpdateMessage = JsonUtility.FromString<AgentRefreshMessage>(message.Body);
|
var runnerUpdateMessage = JsonUtility.FromString<AgentRefreshMessage>(message.Body);
|
||||||
|
#if DEBUG
|
||||||
|
// Can mock the update for testing
|
||||||
|
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_IS_MOCK_UPDATE")))
|
||||||
|
{
|
||||||
|
|
||||||
|
// The mock_update_messages.json file should be an object with keys being the current version and values being the targeted mock version object
|
||||||
|
// Example: { "2.283.2": {"targetVersion":"2.284.1"}, "2.284.1": {"targetVersion":"2.285.0"}}
|
||||||
|
var mockUpdatesPath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), "mock_update_messages.json");
|
||||||
|
if (File.Exists(mockUpdatesPath))
|
||||||
|
{
|
||||||
|
var mockUpdateMessages = JsonUtility.FromString<Dictionary<string, AgentRefreshMessage>>(File.ReadAllText(mockUpdatesPath));
|
||||||
|
if (mockUpdateMessages.ContainsKey(BuildConstants.RunnerPackage.Version))
|
||||||
|
{
|
||||||
|
var mockTargetVersion = mockUpdateMessages[BuildConstants.RunnerPackage.Version].TargetVersion;
|
||||||
|
_term.WriteLine($"Mocking update, using version {mockTargetVersion} instead of {runnerUpdateMessage.TargetVersion}");
|
||||||
|
Trace.Info($"Mocking update, using version {mockTargetVersion} instead of {runnerUpdateMessage.TargetVersion}");
|
||||||
|
runnerUpdateMessage = new AgentRefreshMessage(runnerUpdateMessage.AgentId, mockTargetVersion, runnerUpdateMessage.Timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
var selfUpdater = HostContext.GetService<ISelfUpdater>();
|
var selfUpdater = HostContext.GetService<ISelfUpdater>();
|
||||||
selfUpdateTask = selfUpdater.SelfUpdate(runnerUpdateMessage, jobDispatcher, !runOnce && HostContext.StartupType != StartupType.Service, HostContext.RunnerShutdownToken);
|
selfUpdateTask = selfUpdater.SelfUpdate(runnerUpdateMessage, jobDispatcher, false, HostContext.RunnerShutdownToken);
|
||||||
Trace.Info("Refresh message received, kick-off selfupdate background process.");
|
Trace.Info("Refresh message received, kick-off selfupdate background process.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -425,6 +447,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
Trace.Info($"Received job message of length {message.Body.Length} from service, with hash '{IOUtil.GetSha256Hash(message.Body)}'");
|
||||||
var jobMessage = StringUtil.ConvertFromJson<Pipelines.AgentJobRequestMessage>(message.Body);
|
var jobMessage = StringUtil.ConvertFromJson<Pipelines.AgentJobRequestMessage>(message.Body);
|
||||||
jobDispatcher.Run(jobMessage, runOnce);
|
jobDispatcher.Run(jobMessage, runOnce);
|
||||||
if (runOnce)
|
if (runOnce)
|
||||||
@@ -539,6 +562,7 @@ Config Options:
|
|||||||
--work string Relative runner work directory (default {Constants.Path.WorkDirectory})
|
--work string Relative runner work directory (default {Constants.Path.WorkDirectory})
|
||||||
--replace Replace any existing runner with the same name (default false)
|
--replace Replace any existing runner with the same name (default false)
|
||||||
--pat GitHub personal access token used for checking network connectivity when executing `.{separator}run.{ext} --check`
|
--pat GitHub personal access token used for checking network connectivity when executing `.{separator}run.{ext} --check`
|
||||||
|
--disableupdate Disable self-hosted runner automatic update to the latest released version`
|
||||||
--ephemeral Configure the runner to only take one job and then let the service un-configure the runner after the job finishes (default false)");
|
--ephemeral Configure the runner to only take one job and then let the service un-configure the runner after the job finishes (default false)");
|
||||||
|
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
@@ -546,7 +570,7 @@ Config Options:
|
|||||||
_term.WriteLine($@" --windowslogonaccount string Account to run the service as. Requires runasservice");
|
_term.WriteLine($@" --windowslogonaccount string Account to run the service as. Requires runasservice");
|
||||||
_term.WriteLine($@" --windowslogonpassword string Password for the service account. Requires runasservice");
|
_term.WriteLine($@" --windowslogonpassword string Password for the service account. Requires runasservice");
|
||||||
#endif
|
#endif
|
||||||
_term.WriteLine($@"
|
_term.WriteLine($@"
|
||||||
Examples:
|
Examples:
|
||||||
Check GitHub server network connectivity:
|
Check GitHub server network connectivity:
|
||||||
.{separator}run.{ext} --check --url <url> --pat <pat>
|
.{separator}run.{ext} --check --url <url> --pat <pat>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Security.Cryptography;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using System.Text;
|
using GitHub.Services.Common;
|
||||||
using System.Collections.Generic;
|
using GitHub.Services.WebApi;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener
|
namespace GitHub.Runner.Listener
|
||||||
{
|
{
|
||||||
@@ -30,13 +30,19 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
private static string _packageType = "agent";
|
private static string _packageType = "agent";
|
||||||
private static string _platform = BuildConstants.RunnerPackage.PackageName;
|
private static string _platform = BuildConstants.RunnerPackage.PackageName;
|
||||||
|
private static string _dotnetRuntime = "dotnetRuntime";
|
||||||
|
private static string _externals = "externals";
|
||||||
|
private readonly Dictionary<string, string> _contentHashes = new Dictionary<string, string>();
|
||||||
|
|
||||||
private PackageMetadata _targetPackage;
|
private PackageMetadata _targetPackage;
|
||||||
private ITerminal _terminal;
|
private ITerminal _terminal;
|
||||||
private IRunnerServer _runnerServer;
|
private IRunnerServer _runnerServer;
|
||||||
private int _poolId;
|
private int _poolId;
|
||||||
private int _agentId;
|
private int _agentId;
|
||||||
private readonly List<string> _updateTrace = new List<string>();
|
private readonly ConcurrentQueue<string> _updateTrace = new ConcurrentQueue<string>();
|
||||||
|
private Task _cloneAndCalculateContentHashTask;
|
||||||
|
private string _dotnetRuntimeCloneDirectory;
|
||||||
|
private string _externalsCloneDirectory;
|
||||||
|
|
||||||
public bool Busy { get; private set; }
|
public bool Busy { get; private set; }
|
||||||
|
|
||||||
@@ -50,6 +56,8 @@ namespace GitHub.Runner.Listener
|
|||||||
var settings = configStore.GetSettings();
|
var settings = configStore.GetSettings();
|
||||||
_poolId = settings.PoolId;
|
_poolId = settings.PoolId;
|
||||||
_agentId = settings.AgentId;
|
_agentId = settings.AgentId;
|
||||||
|
_dotnetRuntimeCloneDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), "__dotnet_runtime__");
|
||||||
|
_externalsCloneDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), "__externals__");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SelfUpdate(AgentRefreshMessage updateMessage, IJobDispatcher jobDispatcher, bool restartInteractiveRunner, CancellationToken token)
|
public async Task<bool> SelfUpdate(AgentRefreshMessage updateMessage, IJobDispatcher jobDispatcher, bool restartInteractiveRunner, CancellationToken token)
|
||||||
@@ -59,6 +67,13 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
var totalUpdateTime = Stopwatch.StartNew();
|
var totalUpdateTime = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
// Copy dotnet runtime and externals of current runner to a temp folder
|
||||||
|
// So we can re-use them with trimmed runner package, if possible.
|
||||||
|
// This process is best effort, if we can't use trimmed runner package,
|
||||||
|
// we will just go with the full package.
|
||||||
|
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||||
|
_cloneAndCalculateContentHashTask = CloneAndCalculateAssetsHash(_dotnetRuntimeCloneDirectory, _externalsCloneDirectory, linkedTokenSource.Token);
|
||||||
|
|
||||||
if (!await UpdateNeeded(updateMessage.TargetVersion, token))
|
if (!await UpdateNeeded(updateMessage.TargetVersion, token))
|
||||||
{
|
{
|
||||||
Trace.Info($"Can't find available update package.");
|
Trace.Info($"Can't find available update package.");
|
||||||
@@ -66,13 +81,31 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
|
|
||||||
Trace.Info($"An update is available.");
|
Trace.Info($"An update is available.");
|
||||||
_updateTrace.Add($"RunnerPlatform: {_targetPackage.Platform}");
|
_updateTrace.Enqueue($"RunnerPlatform: {_targetPackage.Platform}");
|
||||||
|
|
||||||
// Print console line that warn user not shutdown runner.
|
// Print console line that warn user not shutdown runner.
|
||||||
await UpdateRunnerUpdateStateAsync("Runner update in progress, do not shutdown runner.");
|
await UpdateRunnerUpdateStateAsync("Runner update in progress, do not shutdown runner.");
|
||||||
await UpdateRunnerUpdateStateAsync($"Downloading {_targetPackage.Version} runner");
|
await UpdateRunnerUpdateStateAsync($"Downloading {_targetPackage.Version} runner");
|
||||||
|
|
||||||
await DownloadLatestRunner(token);
|
if (_targetPackage.TrimmedPackages?.Count > 0)
|
||||||
|
{
|
||||||
|
// wait for cloning assets task to finish only if we have trimmed packages
|
||||||
|
await _cloneAndCalculateContentHashTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
linkedTokenSource.Cancel();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _cloneAndCalculateContentHashTask;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Info($"Ingore errors after cancelling cloning assets task: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await DownloadLatestRunner(token, updateMessage.TargetVersion);
|
||||||
Trace.Info($"Download latest runner and unzip into runner root.");
|
Trace.Info($"Download latest runner and unzip into runner root.");
|
||||||
|
|
||||||
// wait till all running job finish
|
// wait till all running job finish
|
||||||
@@ -88,34 +121,39 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Info($"Delete old version runner backup.");
|
Trace.Info($"Delete old version runner backup.");
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
// generate update script from template
|
// generate update script from template
|
||||||
_updateTrace.Add($"DeleteRunnerBackupTime: {stopWatch.ElapsedMilliseconds}ms");
|
_updateTrace.Enqueue($"DeleteRunnerBackupTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
await UpdateRunnerUpdateStateAsync("Generate and execute update script.");
|
await UpdateRunnerUpdateStateAsync("Generate and execute update script.");
|
||||||
|
|
||||||
string updateScript = GenerateUpdateScript(restartInteractiveRunner);
|
string updateScript = GenerateUpdateScript(restartInteractiveRunner);
|
||||||
Trace.Info($"Generate update script into: {updateScript}");
|
Trace.Info($"Generate update script into: {updateScript}");
|
||||||
|
|
||||||
// kick off update script
|
|
||||||
Process invokeScript = new Process();
|
// For L0, we will skip execute update script.
|
||||||
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_EXECUTE_UPDATE_SCRIPT")))
|
||||||
|
{
|
||||||
|
// kick off update script
|
||||||
|
Process invokeScript = new Process();
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
invokeScript.StartInfo.FileName = WhichUtil.Which("cmd.exe", trace: Trace);
|
invokeScript.StartInfo.FileName = WhichUtil.Which("cmd.exe", trace: Trace);
|
||||||
invokeScript.StartInfo.Arguments = $"/c \"{updateScript}\"";
|
invokeScript.StartInfo.Arguments = $"/c \"{updateScript}\"";
|
||||||
#elif (OS_OSX || OS_LINUX)
|
#elif (OS_OSX || OS_LINUX)
|
||||||
invokeScript.StartInfo.FileName = WhichUtil.Which("bash", trace: Trace);
|
invokeScript.StartInfo.FileName = WhichUtil.Which("bash", trace: Trace);
|
||||||
invokeScript.StartInfo.Arguments = $"\"{updateScript}\"";
|
invokeScript.StartInfo.Arguments = $"\"{updateScript}\"";
|
||||||
#endif
|
#endif
|
||||||
invokeScript.Start();
|
invokeScript.Start();
|
||||||
Trace.Info($"Update script start running");
|
Trace.Info($"Update script start running");
|
||||||
|
}
|
||||||
|
|
||||||
totalUpdateTime.Stop();
|
totalUpdateTime.Stop();
|
||||||
|
|
||||||
_updateTrace.Add($"TotalUpdateTime: {totalUpdateTime.ElapsedMilliseconds}ms");
|
_updateTrace.Enqueue($"TotalUpdateTime: {totalUpdateTime.ElapsedMilliseconds}ms");
|
||||||
await UpdateRunnerUpdateStateAsync("Runner will exit shortly for update, should be back online within 10 seconds.");
|
await UpdateRunnerUpdateStateAsync("Runner will exit shortly for update, should be back online within 10 seconds.");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_updateTrace.Add(ex.ToString());
|
_updateTrace.Enqueue(ex.ToString());
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -169,7 +207,7 @@ namespace GitHub.Runner.Listener
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task DownloadLatestRunner(CancellationToken token)
|
private async Task DownloadLatestRunner(CancellationToken token, string targetVersion)
|
||||||
{
|
{
|
||||||
string latestRunnerDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), Constants.Path.UpdateDirectory);
|
string latestRunnerDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), Constants.Path.UpdateDirectory);
|
||||||
IOUtil.DeleteDirectory(latestRunnerDirectory, token);
|
IOUtil.DeleteDirectory(latestRunnerDirectory, token);
|
||||||
@@ -178,21 +216,117 @@ namespace GitHub.Runner.Listener
|
|||||||
string archiveFile = null;
|
string archiveFile = null;
|
||||||
var packageDownloadUrl = _targetPackage.DownloadUrl;
|
var packageDownloadUrl = _targetPackage.DownloadUrl;
|
||||||
var packageHashValue = _targetPackage.HashValue;
|
var packageHashValue = _targetPackage.HashValue;
|
||||||
_updateTrace.Add($"DownloadUrl: {packageDownloadUrl}");
|
var runtimeTrimmed = false;
|
||||||
|
var externalsTrimmed = false;
|
||||||
|
var fallbackToFullPackage = false;
|
||||||
|
|
||||||
|
// Only try trimmed package if sever sends them and we have calculated hash value of the current runtime/externals.
|
||||||
|
if (_contentHashes.Count == 2 &&
|
||||||
|
_contentHashes.ContainsKey(_dotnetRuntime) &&
|
||||||
|
_contentHashes.ContainsKey(_externals) &&
|
||||||
|
_targetPackage.TrimmedPackages?.Count > 0)
|
||||||
|
{
|
||||||
|
Trace.Info($"Current runner content hash: {StringUtil.ConvertToJson(_contentHashes)}");
|
||||||
|
Trace.Info($"Trimmed packages info from service: {StringUtil.ConvertToJson(_targetPackage.TrimmedPackages)}");
|
||||||
|
// Try to see whether we can use any size trimmed down package to speed up runner updates.
|
||||||
|
foreach (var trimmedPackage in _targetPackage.TrimmedPackages)
|
||||||
|
{
|
||||||
|
if (trimmedPackage.TrimmedContents.Count == 2 &&
|
||||||
|
trimmedPackage.TrimmedContents.TryGetValue(_dotnetRuntime, out var trimmedRuntimeHash) &&
|
||||||
|
trimmedRuntimeHash == _contentHashes[_dotnetRuntime] &&
|
||||||
|
trimmedPackage.TrimmedContents.TryGetValue(_externals, out var trimmedExternalsHash) &&
|
||||||
|
trimmedExternalsHash == _contentHashes[_externals])
|
||||||
|
{
|
||||||
|
Trace.Info($"Use trimmed (runtime+externals) package '{trimmedPackage.DownloadUrl}' to update runner.");
|
||||||
|
packageDownloadUrl = trimmedPackage.DownloadUrl;
|
||||||
|
packageHashValue = trimmedPackage.HashValue;
|
||||||
|
runtimeTrimmed = true;
|
||||||
|
externalsTrimmed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (trimmedPackage.TrimmedContents.Count == 1 &&
|
||||||
|
trimmedPackage.TrimmedContents.TryGetValue(_externals, out trimmedExternalsHash) &&
|
||||||
|
trimmedExternalsHash == _contentHashes[_externals])
|
||||||
|
{
|
||||||
|
Trace.Info($"Use trimmed (externals) package '{trimmedPackage.DownloadUrl}' to update runner.");
|
||||||
|
packageDownloadUrl = trimmedPackage.DownloadUrl;
|
||||||
|
packageHashValue = trimmedPackage.HashValue;
|
||||||
|
externalsTrimmed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"Can't use trimmed package from '{trimmedPackage.DownloadUrl}' since the current runner does not carry those trimmed content (Hash mismatch).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateTrace.Enqueue($"DownloadUrl: {packageDownloadUrl}");
|
||||||
|
_updateTrace.Enqueue($"RuntimeTrimmed: {runtimeTrimmed}");
|
||||||
|
_updateTrace.Enqueue($"ExternalsTrimmed: {externalsTrimmed}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
archiveFile = await DownLoadRunner(latestRunnerDirectory, packageDownloadUrl, packageHashValue, token);
|
#if DEBUG
|
||||||
|
// Much of the update process (targetVersion, archive) is server-side, this is a way to control it from here for testing specific update scenarios
|
||||||
|
// Add files like 'runner2.281.2.tar.gz' or 'runner2.283.0.zip' (depending on your platform) to your runner root folder
|
||||||
|
// Note that runners still need to be older than the server's runner version in order to receive an 'AgentRefreshMessage' and trigger this update
|
||||||
|
// Wrapped in #if DEBUG as this should not be in the RELEASE build
|
||||||
|
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_IS_MOCK_UPDATE")))
|
||||||
|
{
|
||||||
|
var waitForDebugger = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_IS_MOCK_UPDATE_WAIT_FOR_DEBUGGER"));
|
||||||
|
if (waitForDebugger)
|
||||||
|
{
|
||||||
|
int waitInSeconds = 20;
|
||||||
|
while (!Debugger.IsAttached && waitInSeconds-- > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
Debugger.Break();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_targetPackage.Platform.StartsWith("win"))
|
||||||
|
{
|
||||||
|
archiveFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), $"runner{targetVersion}.zip");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
archiveFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), $"runner{targetVersion}.tar.gz");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(archiveFile))
|
||||||
|
{
|
||||||
|
_updateTrace.Enqueue($"Mocking update with file: '{archiveFile}' and targetVersion: '{targetVersion}', nothing is downloaded");
|
||||||
|
_terminal.WriteLine($"Mocking update with file: '{archiveFile}' and targetVersion: '{targetVersion}', nothing is downloaded");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
archiveFile = null;
|
||||||
|
_terminal.WriteLine($"Mock runner archive not found at {archiveFile} for target version {targetVersion}, proceeding with download instead");
|
||||||
|
_updateTrace.Enqueue($"Mock runner archive not found at {archiveFile} for target version {targetVersion}, proceeding with download instead");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// archiveFile is not null only if we mocked it above
|
||||||
if (string.IsNullOrEmpty(archiveFile))
|
if (string.IsNullOrEmpty(archiveFile))
|
||||||
{
|
{
|
||||||
throw new TaskCanceledException($"Runner package '{packageDownloadUrl}' failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts");
|
archiveFile = await DownLoadRunner(latestRunnerDirectory, packageDownloadUrl, packageHashValue, token);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(archiveFile))
|
||||||
|
{
|
||||||
|
throw new TaskCanceledException($"Runner package '{packageDownloadUrl}' failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts");
|
||||||
|
}
|
||||||
|
await ValidateRunnerHash(archiveFile, packageHashValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ValidateRunnerHash(archiveFile, packageHashValue);
|
|
||||||
|
|
||||||
await ExtractRunnerPackage(archiveFile, latestRunnerDirectory, token);
|
await ExtractRunnerPackage(archiveFile, latestRunnerDirectory, token);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex) when (runtimeTrimmed || externalsTrimmed)
|
||||||
|
{
|
||||||
|
// if anything failed when we use trimmed package (download/validatehase/extract), try again with the full runner package.
|
||||||
|
Trace.Error($"Fail to download latest runner using trimmed package: {ex}");
|
||||||
|
fallbackToFullPackage = true;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -211,6 +345,74 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var trimmedPackageRestoreTasks = new List<Task<bool>>();
|
||||||
|
if (!fallbackToFullPackage)
|
||||||
|
{
|
||||||
|
// Skip restoring externals and runtime if we are going to fullback to the full package.
|
||||||
|
if (externalsTrimmed)
|
||||||
|
{
|
||||||
|
trimmedPackageRestoreTasks.Add(RestoreTrimmedExternals(latestRunnerDirectory, token));
|
||||||
|
}
|
||||||
|
if (runtimeTrimmed)
|
||||||
|
{
|
||||||
|
trimmedPackageRestoreTasks.Add(RestoreTrimmedDotnetRuntime(latestRunnerDirectory, token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedPackageRestoreTasks.Count > 0)
|
||||||
|
{
|
||||||
|
var restoreResults = await Task.WhenAll(trimmedPackageRestoreTasks);
|
||||||
|
if (restoreResults.Any(x => x == false))
|
||||||
|
{
|
||||||
|
// if any of the restore failed, fallback to full package.
|
||||||
|
fallbackToFullPackage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackToFullPackage)
|
||||||
|
{
|
||||||
|
Trace.Error("Something wrong with the trimmed runner package, failback to use the full package for runner updates.");
|
||||||
|
_updateTrace.Enqueue($"FallbackToFullPackage: {fallbackToFullPackage}");
|
||||||
|
|
||||||
|
IOUtil.DeleteDirectory(latestRunnerDirectory, token);
|
||||||
|
Directory.CreateDirectory(latestRunnerDirectory);
|
||||||
|
|
||||||
|
packageDownloadUrl = _targetPackage.DownloadUrl;
|
||||||
|
packageHashValue = _targetPackage.HashValue;
|
||||||
|
_updateTrace.Enqueue($"DownloadUrl: {packageDownloadUrl}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
archiveFile = await DownLoadRunner(latestRunnerDirectory, packageDownloadUrl, packageHashValue, token);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(archiveFile))
|
||||||
|
{
|
||||||
|
throw new TaskCanceledException($"Runner package '{packageDownloadUrl}' failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts");
|
||||||
|
}
|
||||||
|
|
||||||
|
await ValidateRunnerHash(archiveFile, packageHashValue);
|
||||||
|
|
||||||
|
await ExtractRunnerPackage(archiveFile, latestRunnerDirectory, token);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// delete .zip file
|
||||||
|
if (!string.IsNullOrEmpty(archiveFile) && File.Exists(archiveFile))
|
||||||
|
{
|
||||||
|
Trace.Verbose("Deleting latest runner package zip: {0}", archiveFile);
|
||||||
|
IOUtil.DeleteFile(archiveFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//it is not critical if we fail to delete the .zip file
|
||||||
|
Trace.Warning("Failed to delete runner package zip '{0}'. Exception: {1}", archiveFile, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await CopyLatestRunnerToRoot(latestRunnerDirectory, token);
|
await CopyLatestRunnerToRoot(latestRunnerDirectory, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,14 +497,14 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Info($"Download runner: finished download");
|
Trace.Info($"Download runner: finished download");
|
||||||
downloadSucceeded = true;
|
downloadSucceeded = true;
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
_updateTrace.Add($"PackageDownloadTime: {stopWatch.ElapsedMilliseconds}ms");
|
_updateTrace.Enqueue($"PackageDownloadTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
_updateTrace.Add($"Attempts: {attempt}");
|
_updateTrace.Enqueue($"Attempts: {attempt}");
|
||||||
_updateTrace.Add($"PackageSize: {downloadSize / 1024 / 1024}MB");
|
_updateTrace.Enqueue($"PackageSize: {downloadSize / 1024 / 1024}MB");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
Trace.Info($"Runner download has been canceled.");
|
Trace.Info($"Runner download has been cancelled.");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -342,12 +544,12 @@ namespace GitHub.Runner.Listener
|
|||||||
if (hash != packageHashValue)
|
if (hash != packageHashValue)
|
||||||
{
|
{
|
||||||
// Hash did not match, we can't recover from this, just throw
|
// Hash did not match, we can't recover from this, just throw
|
||||||
throw new Exception($"Computed runner hash {hash} did not match expected Runner Hash {packageHashValue} for {_targetPackage.Filename}");
|
throw new Exception($"Computed runner hash {hash} did not match expected Runner Hash {packageHashValue} for {archiveFile}");
|
||||||
}
|
}
|
||||||
|
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
Trace.Info($"Validated Runner Hash matches {_targetPackage.Filename} : {packageHashValue}");
|
Trace.Info($"Validated Runner Hash matches {archiveFile} : {packageHashValue}");
|
||||||
_updateTrace.Add($"ValidateHashTime: {stopWatch.ElapsedMilliseconds}ms");
|
_updateTrace.Enqueue($"ValidateHashTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,7 +605,7 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
Trace.Info($"Finished getting latest runner package at: {extractDirectory}.");
|
Trace.Info($"Finished getting latest runner package at: {extractDirectory}.");
|
||||||
_updateTrace.Add($"PackageExtractTime: {stopWatch.ElapsedMilliseconds}ms");
|
_updateTrace.Enqueue($"PackageExtractTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task CopyLatestRunnerToRoot(string latestRunnerDirectory, CancellationToken token)
|
private Task CopyLatestRunnerToRoot(string latestRunnerDirectory, CancellationToken token)
|
||||||
@@ -436,7 +638,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
|
|
||||||
stopWatch.Stop();
|
stopWatch.Stop();
|
||||||
_updateTrace.Add($"CopyRunnerToRootTime: {stopWatch.ElapsedMilliseconds}ms");
|
_updateTrace.Enqueue($"CopyRunnerToRootTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,9 +763,15 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
_terminal.WriteLine(currentState);
|
_terminal.WriteLine(currentState);
|
||||||
|
|
||||||
if (_updateTrace.Count > 0)
|
var traces = new List<string>();
|
||||||
|
while (_updateTrace.TryDequeue(out var trace))
|
||||||
{
|
{
|
||||||
foreach (var trace in _updateTrace)
|
traces.Add(trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (traces.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var trace in traces)
|
||||||
{
|
{
|
||||||
Trace.Info(trace);
|
Trace.Info(trace);
|
||||||
}
|
}
|
||||||
@@ -571,7 +779,7 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _runnerServer.UpdateAgentUpdateStateAsync(_poolId, _agentId, currentState, string.Join(Environment.NewLine, _updateTrace));
|
await _runnerServer.UpdateAgentUpdateStateAsync(_poolId, _agentId, currentState, string.Join(Environment.NewLine, traces));
|
||||||
_updateTrace.Clear();
|
_updateTrace.Clear();
|
||||||
}
|
}
|
||||||
catch (VssResourceNotFoundException)
|
catch (VssResourceNotFoundException)
|
||||||
@@ -585,5 +793,330 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Info($"Catch exception during report update state, ignore this error and continue auto-update.");
|
Trace.Info($"Catch exception during report update state, ignore this error and continue auto-update.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> RestoreTrimmedExternals(string downloadDirectory, CancellationToken token)
|
||||||
|
{
|
||||||
|
// Copy the current runner's externals if we are using a externals trimmed package
|
||||||
|
// Execute the node.js to make sure the copied externals is working.
|
||||||
|
var stopWatch = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Trace.Info($"Copy {_externalsCloneDirectory} to {Path.Combine(downloadDirectory, Constants.Path.ExternalsDirectory)}.");
|
||||||
|
IOUtil.CopyDirectory(_externalsCloneDirectory, Path.Combine(downloadDirectory, Constants.Path.ExternalsDirectory), token);
|
||||||
|
|
||||||
|
// try run node.js to see if current node.js works fine after copy over to new location.
|
||||||
|
var nodeVersions = NodeUtil.BuiltInNodeVersions;
|
||||||
|
foreach (var nodeVersion in nodeVersions)
|
||||||
|
{
|
||||||
|
var newNodeBinary = Path.Combine(downloadDirectory, Constants.Path.ExternalsDirectory, nodeVersion, "bin", $"node{IOUtil.ExeExtension}");
|
||||||
|
if (File.Exists(newNodeBinary))
|
||||||
|
{
|
||||||
|
using (var p = HostContext.CreateService<IProcessInvoker>())
|
||||||
|
{
|
||||||
|
var outputs = "";
|
||||||
|
p.ErrorDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data.Data))
|
||||||
|
{
|
||||||
|
Trace.Error(data.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
p.OutputDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data.Data))
|
||||||
|
{
|
||||||
|
Trace.Info(data.Data);
|
||||||
|
outputs = data.Data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var exitCode = await p.ExecuteAsync(HostContext.GetDirectory(WellKnownDirectory.Root), newNodeBinary, $"-e \"console.log('{nameof(RestoreTrimmedExternals)}')\"", null, token);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
Trace.Error($"{newNodeBinary} -e \"console.log()\" failed with exit code {exitCode}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(outputs, nameof(RestoreTrimmedExternals), StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Trace.Error($"{newNodeBinary} -e \"console.log()\" did not output expected content.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to restore externals for trimmed package: {ex}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stopWatch.Stop();
|
||||||
|
_updateTrace.Enqueue($"{nameof(RestoreTrimmedExternals)}Time: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> RestoreTrimmedDotnetRuntime(string downloadDirectory, CancellationToken token)
|
||||||
|
{
|
||||||
|
// Copy the current runner's dotnet runtime if we are using a dotnet runtime trimmed package
|
||||||
|
// Execute the runner.listener to make sure the copied runtime is working.
|
||||||
|
var stopWatch = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Trace.Info($"Copy {_dotnetRuntimeCloneDirectory} to {Path.Combine(downloadDirectory, Constants.Path.BinDirectory)}.");
|
||||||
|
IOUtil.CopyDirectory(_dotnetRuntimeCloneDirectory, Path.Combine(downloadDirectory, Constants.Path.BinDirectory), token);
|
||||||
|
|
||||||
|
// try run the runner executable to see if current dotnet runtime + future runner binary works fine.
|
||||||
|
var newRunnerBinary = Path.Combine(downloadDirectory, Constants.Path.BinDirectory, "Runner.Listener");
|
||||||
|
using (var p = HostContext.CreateService<IProcessInvoker>())
|
||||||
|
{
|
||||||
|
p.ErrorDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data.Data))
|
||||||
|
{
|
||||||
|
Trace.Error(data.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
p.OutputDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data.Data))
|
||||||
|
{
|
||||||
|
Trace.Info(data.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var exitCode = await p.ExecuteAsync(HostContext.GetDirectory(WellKnownDirectory.Root), newRunnerBinary, "--version", null, token);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
Trace.Error($"{newRunnerBinary} --version failed with exit code {exitCode}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to restore dotnet runtime for trimmed package: {ex}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stopWatch.Stop();
|
||||||
|
_updateTrace.Enqueue($"{nameof(RestoreTrimmedDotnetRuntime)}Time: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloneAndCalculateAssetsHash(string dotnetRuntimeCloneDirectory, string externalsCloneDirectory, CancellationToken token)
|
||||||
|
{
|
||||||
|
var runtimeCloneTask = CloneDotnetRuntime(dotnetRuntimeCloneDirectory, token);
|
||||||
|
var externalsCloneTask = CloneExternals(externalsCloneDirectory, token);
|
||||||
|
|
||||||
|
var waitingTasks = new Dictionary<string, Task>()
|
||||||
|
{
|
||||||
|
{nameof(CloneDotnetRuntime), runtimeCloneTask},
|
||||||
|
{nameof(CloneExternals),externalsCloneTask}
|
||||||
|
};
|
||||||
|
|
||||||
|
while (waitingTasks.Count > 0)
|
||||||
|
{
|
||||||
|
Trace.Info($"Waiting for {waitingTasks.Count} tasks to complete.");
|
||||||
|
var complatedTask = await Task.WhenAny(waitingTasks.Values);
|
||||||
|
if (waitingTasks.ContainsKey(nameof(CloneExternals)) &&
|
||||||
|
complatedTask == waitingTasks[nameof(CloneExternals)])
|
||||||
|
{
|
||||||
|
Trace.Info($"Externals clone finished.");
|
||||||
|
waitingTasks.Remove(nameof(CloneExternals));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await externalsCloneTask && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var externalsHash = await HashFiles(externalsCloneDirectory, token);
|
||||||
|
Trace.Info($"Externals content hash: {externalsHash}");
|
||||||
|
_contentHashes[_externals] = externalsHash;
|
||||||
|
_updateTrace.Enqueue($"ExternalsHash: {_contentHashes[_externals]}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Error($"Skip compute hash since clone externals failed/cancelled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to hash externals content: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (waitingTasks.ContainsKey(nameof(CloneDotnetRuntime)) &&
|
||||||
|
complatedTask == waitingTasks[nameof(CloneDotnetRuntime)])
|
||||||
|
{
|
||||||
|
Trace.Info($"Dotnet runtime clone finished.");
|
||||||
|
waitingTasks.Remove(nameof(CloneDotnetRuntime));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await runtimeCloneTask && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var runtimeHash = await HashFiles(dotnetRuntimeCloneDirectory, token);
|
||||||
|
Trace.Info($"Runtime content hash: {runtimeHash}");
|
||||||
|
_contentHashes[_dotnetRuntime] = runtimeHash;
|
||||||
|
_updateTrace.Enqueue($"DotnetRuntimeHash: {_contentHashes[_dotnetRuntime]}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Error($"Skip compute hash since clone dotnet runtime failed/cancelled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to hash runtime content: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Info($"Still waiting for {waitingTasks.Count} tasks to complete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CloneDotnetRuntime(string runtimeDir, CancellationToken token)
|
||||||
|
{
|
||||||
|
var stopWatch = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Trace.Info($"Cloning dotnet runtime to {runtimeDir}");
|
||||||
|
IOUtil.DeleteDirectory(runtimeDir, CancellationToken.None);
|
||||||
|
Directory.CreateDirectory(runtimeDir);
|
||||||
|
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var assetsContent = default(string);
|
||||||
|
using (var stream = assembly.GetManifestResourceStream("GitHub.Runner.Listener.runnercoreassets"))
|
||||||
|
using (var streamReader = new StreamReader(stream))
|
||||||
|
{
|
||||||
|
assetsContent = await streamReader.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(assetsContent))
|
||||||
|
{
|
||||||
|
var runnerCoreAssets = assetsContent.Split(new[] { "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (runnerCoreAssets.Length > 0)
|
||||||
|
{
|
||||||
|
var binDir = HostContext.GetDirectory(WellKnownDirectory.Bin);
|
||||||
|
IOUtil.CopyDirectory(binDir, runtimeDir, token);
|
||||||
|
|
||||||
|
var clonedFile = 0;
|
||||||
|
foreach (var file in Directory.EnumerateFiles(runtimeDir, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
if (runnerCoreAssets.Any(x => file.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).EndsWith(x.Trim())))
|
||||||
|
{
|
||||||
|
Trace.Verbose($"{file} is part of the runner core, delete from cloned runtime directory.");
|
||||||
|
IOUtil.DeleteFile(file);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clonedFile++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Info($"Successfully cloned dotnet runtime to {runtimeDir}. Total files: {clonedFile}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to clone dotnet runtime to {runtimeDir}");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stopWatch.Stop();
|
||||||
|
_updateTrace.Enqueue($"{nameof(CloneDotnetRuntime)}Time: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> CloneExternals(string externalsDir, CancellationToken token)
|
||||||
|
{
|
||||||
|
var stopWatch = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Trace.Info($"Cloning externals to {externalsDir}");
|
||||||
|
IOUtil.DeleteDirectory(externalsDir, CancellationToken.None);
|
||||||
|
Directory.CreateDirectory(externalsDir);
|
||||||
|
IOUtil.CopyDirectory(HostContext.GetDirectory(WellKnownDirectory.Externals), externalsDir, token);
|
||||||
|
Trace.Info($"Successfully cloned externals to {externalsDir}.");
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to clone externals to {externalsDir}");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stopWatch.Stop();
|
||||||
|
_updateTrace.Enqueue($"{nameof(CloneExternals)}Time: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> HashFiles(string fileFolder, CancellationToken token)
|
||||||
|
{
|
||||||
|
Trace.Info($"Calculating hash for {fileFolder}");
|
||||||
|
|
||||||
|
var stopWatch = Stopwatch.StartNew();
|
||||||
|
string binDir = HostContext.GetDirectory(WellKnownDirectory.Bin);
|
||||||
|
string node = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}");
|
||||||
|
string hashFilesScript = Path.Combine(binDir, "hashFiles");
|
||||||
|
var hashResult = string.Empty;
|
||||||
|
|
||||||
|
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
|
||||||
|
{
|
||||||
|
processInvoker.ErrorDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data.Data) && data.Data.StartsWith("__OUTPUT__") && data.Data.EndsWith("__OUTPUT__"))
|
||||||
|
{
|
||||||
|
hashResult = data.Data.Substring(10, data.Data.Length - 20);
|
||||||
|
Trace.Info($"Hash result: '{hashResult}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info(data.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processInvoker.OutputDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
Trace.Verbose(data.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
var env = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["patterns"] = "**"
|
||||||
|
};
|
||||||
|
|
||||||
|
int exitCode = await processInvoker.ExecuteAsync(workingDirectory: fileFolder,
|
||||||
|
fileName: node,
|
||||||
|
arguments: $"\"{hashFilesScript.Replace("\"", "\\\"")}\"",
|
||||||
|
environment: env,
|
||||||
|
requireExitCodeZero: false,
|
||||||
|
outputEncoding: null,
|
||||||
|
killProcessOnCancel: true,
|
||||||
|
cancellationToken: token);
|
||||||
|
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
Trace.Error($"hashFiles returns '{exitCode}' failed. Fail to hash files under directory '{fileFolder}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWatch.Stop();
|
||||||
|
_updateTrace.Enqueue($"{nameof(HashFiles)}{Path.GetFileName(fileFolder)}Time: {stopWatch.ElapsedMilliseconds}ms");
|
||||||
|
return hashResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
<NoWarn>NU1701;NU1603</NoWarn>
|
<NoWarn>NU1701;NU1603</NoWarn>
|
||||||
<Version>$(Version)</Version>
|
<Version>$(Version)</Version>
|
||||||
|
<PredefinedCulturesOnly>false</PredefinedCulturesOnly>
|
||||||
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
|
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ namespace GitHub.Runner.Plugins.Artifact
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
uploadTimer.Restart();
|
uploadTimer.Restart();
|
||||||
using (HttpResponseMessage response = await _fileContainerHttpClient.UploadFileAsync(_containerId, itemPath, fs, _projectId, cancellationToken: token, chunkSize: 4 * 1024 * 1024))
|
using (HttpResponseMessage response = await _fileContainerHttpClient.UploadFileAsync(_containerId, itemPath, fs, _projectId, cancellationToken: token))
|
||||||
{
|
{
|
||||||
if (response == null || response.StatusCode != HttpStatusCode.Created)
|
if (response == null || response.StatusCode != HttpStatusCode.Created)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// delete the index.lock file left by previous canceled build or any operation cause git.exe crash last time.
|
// delete the index.lock file left by previous cancelled build or any operation cause git.exe crash last time.
|
||||||
string lockFile = Path.Combine(targetPath, ".git\\index.lock");
|
string lockFile = Path.Combine(targetPath, ".git\\index.lock");
|
||||||
if (File.Exists(lockFile))
|
if (File.Exists(lockFile))
|
||||||
{
|
{
|
||||||
@@ -181,7 +181,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the shallow.lock file left by previous canceled build or any operation cause git.exe crash last time.
|
// delete the shallow.lock file left by previous cancelled build or any operation cause git.exe crash last time.
|
||||||
string shallowLockFile = Path.Combine(targetPath, ".git\\shallow.lock");
|
string shallowLockFile = Path.Combine(targetPath, ".git\\shallow.lock");
|
||||||
if (File.Exists(shallowLockFile))
|
if (File.Exists(shallowLockFile))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// delete the index.lock file left by previous canceled build or any operation cause git.exe crash last time.
|
// delete the index.lock file left by previous cancelled build or any operation cause git.exe crash last time.
|
||||||
string lockFile = Path.Combine(targetPath, ".git\\index.lock");
|
string lockFile = Path.Combine(targetPath, ".git\\index.lock");
|
||||||
if (File.Exists(lockFile))
|
if (File.Exists(lockFile))
|
||||||
{
|
{
|
||||||
@@ -165,7 +165,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the shallow.lock file left by previous canceled build or any operation cause git.exe crash last time.
|
// delete the shallow.lock file left by previous cancelled build or any operation cause git.exe crash last time.
|
||||||
string shallowLockFile = Path.Combine(targetPath, ".git\\shallow.lock");
|
string shallowLockFile = Path.Combine(targetPath, ".git\\shallow.lock");
|
||||||
if (File.Exists(shallowLockFile))
|
if (File.Exists(shallowLockFile))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ namespace GitHub.Runner.Sdk
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new token source for the parallel query. The parallel query should be
|
// Create a new token source for the parallel query. The parallel query should be
|
||||||
// canceled after the first error is encountered. Otherwise the number of exceptions
|
// cancelled after the first error is encountered. Otherwise the number of exceptions
|
||||||
// could get out of control for a large directory with access denied on every file.
|
// could get out of control for a large directory with access denied on every file.
|
||||||
using (var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
using (var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ namespace GitHub.Runner.Sdk
|
|||||||
|
|
||||||
VssClientHttpRequestSettings.Default.UserAgent = headerValues;
|
VssClientHttpRequestSettings.Default.UserAgent = headerValues;
|
||||||
VssHttpMessageHandler.DefaultWebProxy = proxy;
|
VssHttpMessageHandler.DefaultWebProxy = proxy;
|
||||||
|
|
||||||
|
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
|
||||||
|
{
|
||||||
|
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static VssConnection CreateConnection(Uri serverUri, VssCredentials credentials, IEnumerable<DelegatingHandler> additionalDelegatingHandler = null, TimeSpan? timeout = null)
|
public static VssConnection CreateConnection(Uri serverUri, VssCredentials credentials, IEnumerable<DelegatingHandler> additionalDelegatingHandler = null, TimeSpan? timeout = null)
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ namespace GitHub.Runner.Worker
|
|||||||
Message = $"Invoked ::stopCommand:: with token: [{stopToken}]",
|
Message = $"Invoked ::stopCommand:: with token: [{stopToken}]",
|
||||||
Type = JobTelemetryType.ActionCommand
|
Type = JobTelemetryType.ActionCommand
|
||||||
};
|
};
|
||||||
context.JobTelemetry.Add(telemetry);
|
context.Global.JobTelemetry.Add(telemetry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTokenInvalid && !allowUnsecureStopCommandTokens)
|
if (isTokenInvalid && !allowUnsecureStopCommandTokens)
|
||||||
|
|||||||
@@ -651,10 +651,10 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Global.Plan.ScopeIdentifier, executionContext.Global.Plan.PlanType, executionContext.Global.Plan.PlanId, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
|
actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Global.Plan.ScopeIdentifier, executionContext.Global.Plan.PlanType, executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (!executionContext.CancellationToken.IsCancellationRequested) // Do not retry if the run is canceled.
|
catch (Exception ex) when (!executionContext.CancellationToken.IsCancellationRequested) // Do not retry if the run is cancelled.
|
||||||
{
|
{
|
||||||
// UnresolvableActionDownloadInfoException is a 422 client error, don't retry
|
// UnresolvableActionDownloadInfoException is a 422 client error, don't retry
|
||||||
// Some possible cases are:
|
// Some possible cases are:
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.ObjectTemplating;
|
using GitHub.DistributedTask.ObjectTemplating;
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
using GitHub.DistributedTask.Pipelines;
|
using GitHub.DistributedTask.Pipelines;
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Handlers;
|
using GitHub.Runner.Worker.Handlers;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
using GitHub.Runner.Common;
|
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
namespace GitHub.Runner.Worker
|
||||||
{
|
{
|
||||||
@@ -141,21 +140,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
IStepHost stepHost = HostContext.CreateService<IDefaultStepHost>();
|
IStepHost stepHost = HostContext.CreateService<IDefaultStepHost>();
|
||||||
|
|
||||||
// Makes directory for event_path data
|
ExecutionContext.WriteWebhookPayload();
|
||||||
var tempDirectory = HostContext.GetDirectory(WellKnownDirectory.Temp);
|
|
||||||
var workflowDirectory = Path.Combine(tempDirectory, "_github_workflow");
|
|
||||||
Directory.CreateDirectory(workflowDirectory);
|
|
||||||
|
|
||||||
var gitHubEvent = ExecutionContext.GetGitHubContext("event");
|
|
||||||
|
|
||||||
// adds the GitHub event path/file if the event exists
|
|
||||||
if (gitHubEvent != null)
|
|
||||||
{
|
|
||||||
var workflowFile = Path.Combine(workflowDirectory, "event.json");
|
|
||||||
Trace.Info($"Write event payload to {workflowFile}");
|
|
||||||
File.WriteAllText(workflowFile, gitHubEvent, new UTF8Encoding(false));
|
|
||||||
ExecutionContext.SetGitHubContext("event_path", workflowFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set GITHUB_ACTION_REPOSITORY if this Action is from a repository
|
// Set GITHUB_ACTION_REPOSITORY if this Action is from a repository
|
||||||
if (Action.Reference is Pipelines.RepositoryPathReference repoPathReferenceAction &&
|
if (Action.Reference is Pipelines.RepositoryPathReference repoPathReferenceAction &&
|
||||||
@@ -186,8 +171,16 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
// Load the inputs.
|
// Load the inputs.
|
||||||
ExecutionContext.Debug("Loading inputs");
|
ExecutionContext.Debug("Loading inputs");
|
||||||
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
|
Dictionary<string, string> inputs;
|
||||||
var inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
|
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.UseContainerPathForTemplate) ?? false)
|
||||||
|
{
|
||||||
|
inputs = EvaluateStepInputs(stepHost);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
|
||||||
|
inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
|
||||||
|
}
|
||||||
|
|
||||||
var userInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var userInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (KeyValuePair<string, string> input in inputs)
|
foreach (KeyValuePair<string, string> input in inputs)
|
||||||
@@ -274,8 +267,8 @@ namespace GitHub.Runner.Worker
|
|||||||
actionDirectory: definition.Directory,
|
actionDirectory: definition.Directory,
|
||||||
localActionContainerSetupSteps: localActionContainerSetupSteps);
|
localActionContainerSetupSteps: localActionContainerSetupSteps);
|
||||||
|
|
||||||
// Print out action details
|
// Print out action details and log telemetry
|
||||||
handler.PrintActionDetails(Stage);
|
handler.PrepareExecution(Stage);
|
||||||
|
|
||||||
// Run the task.
|
// Run the task.
|
||||||
try
|
try
|
||||||
@@ -314,6 +307,15 @@ namespace GitHub.Runner.Worker
|
|||||||
return didFullyEvaluate;
|
return didFullyEvaluate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Dictionary<String, String> EvaluateStepInputs(IStepHost stepHost)
|
||||||
|
{
|
||||||
|
DictionaryContextData expressionValues = ExecutionContext.GetExpressionValues(stepHost);
|
||||||
|
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
|
||||||
|
var inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, expressionValues, ExecutionContext.ExpressionFunctions);
|
||||||
|
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
|
||||||
private string GenerateDisplayName(ActionStep action, DictionaryContextData contextData, IExecutionContext context, out bool didFullyEvaluate)
|
private string GenerateDisplayName(ActionStep action, DictionaryContextData contextData, IExecutionContext context, out bool didFullyEvaluate)
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(context, nameof(context));
|
ArgUtil.NotNull(context, nameof(context));
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
|
||||||
using GitHub.DistributedTask.Expressions2;
|
using GitHub.DistributedTask.Expressions2;
|
||||||
using GitHub.DistributedTask.Pipelines;
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.Runner.Worker.Container;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Runner.Worker.Handlers;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
@@ -52,8 +48,7 @@ namespace GitHub.Runner.Worker
|
|||||||
Dictionary<string, string> IntraActionState { get; }
|
Dictionary<string, string> IntraActionState { get; }
|
||||||
Dictionary<string, VariableValue> JobOutputs { get; }
|
Dictionary<string, VariableValue> JobOutputs { get; }
|
||||||
ActionsEnvironmentReference ActionsEnvironment { get; }
|
ActionsEnvironmentReference ActionsEnvironment { get; }
|
||||||
List<ActionsStepTelemetry> ActionsStepsTelemetry { get; }
|
ActionsStepTelemetry StepTelemetry { get; }
|
||||||
List<JobTelemetry> JobTelemetry { get; }
|
|
||||||
DictionaryContextData ExpressionValues { get; }
|
DictionaryContextData ExpressionValues { get; }
|
||||||
IList<IFunctionInfo> ExpressionFunctions { get; }
|
IList<IFunctionInfo> ExpressionFunctions { get; }
|
||||||
JobContext JobContext { get; }
|
JobContext JobContext { get; }
|
||||||
@@ -109,12 +104,21 @@ namespace GitHub.Runner.Worker
|
|||||||
// others
|
// others
|
||||||
void ForceTaskComplete();
|
void ForceTaskComplete();
|
||||||
void RegisterPostJobStep(IStep step);
|
void RegisterPostJobStep(IStep step);
|
||||||
|
void PublishStepTelemetry();
|
||||||
|
|
||||||
|
void ApplyContinueOnError(TemplateToken continueOnError);
|
||||||
|
void UpdateGlobalStepsContext();
|
||||||
|
|
||||||
|
void WriteWebhookPayload();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ExecutionContext : RunnerService, IExecutionContext
|
public sealed class ExecutionContext : RunnerService, IExecutionContext
|
||||||
{
|
{
|
||||||
private const int _maxIssueCount = 10;
|
private const int _maxIssueCount = 10;
|
||||||
private const int _throttlingDelayReportThreshold = 10 * 1000; // Don't report throttling with less than 10 seconds delay
|
private const int _throttlingDelayReportThreshold = 10 * 1000; // Don't report throttling with less than 10 seconds delay
|
||||||
|
private const int _maxIssueMessageLength = 4096; // Don't send issue with huge message since we can't forward them from actions to check annotation.
|
||||||
|
private const int _maxIssueCountInTelemetry = 3; // Only send the first 3 issues to telemetry
|
||||||
|
private const int _maxIssueMessageLengthInTelemetry = 256; // Only send the first 256 characters of issue message to telemetry
|
||||||
|
|
||||||
private readonly TimelineRecord _record = new TimelineRecord();
|
private readonly TimelineRecord _record = new TimelineRecord();
|
||||||
private readonly Dictionary<Guid, TimelineRecord> _detailRecords = new Dictionary<Guid, TimelineRecord>();
|
private readonly Dictionary<Guid, TimelineRecord> _detailRecords = new Dictionary<Guid, TimelineRecord>();
|
||||||
@@ -139,6 +143,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
// only job level ExecutionContext will track throttling delay.
|
// only job level ExecutionContext will track throttling delay.
|
||||||
private long _totalThrottlingDelayInMilliseconds = 0;
|
private long _totalThrottlingDelayInMilliseconds = 0;
|
||||||
|
private bool _stepTelemetryPublished = false;
|
||||||
|
|
||||||
public Guid Id => _record.Id;
|
public Guid Id => _record.Id;
|
||||||
public Guid EmbeddedId { get; private set; }
|
public Guid EmbeddedId { get; private set; }
|
||||||
@@ -152,8 +157,7 @@ namespace GitHub.Runner.Worker
|
|||||||
public Dictionary<string, VariableValue> JobOutputs { get; private set; }
|
public Dictionary<string, VariableValue> JobOutputs { get; private set; }
|
||||||
|
|
||||||
public ActionsEnvironmentReference ActionsEnvironment { get; private set; }
|
public ActionsEnvironmentReference ActionsEnvironment { get; private set; }
|
||||||
public List<ActionsStepTelemetry> ActionsStepsTelemetry { get; private set; }
|
public ActionsStepTelemetry StepTelemetry { get; } = new ActionsStepTelemetry();
|
||||||
public List<JobTelemetry> JobTelemetry { get; private set; }
|
|
||||||
public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData();
|
public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData();
|
||||||
public IList<IFunctionInfo> ExpressionFunctions { get; } = new List<IFunctionInfo>();
|
public IList<IFunctionInfo> ExpressionFunctions { get; } = new List<IFunctionInfo>();
|
||||||
|
|
||||||
@@ -273,9 +277,9 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
Trace.Info($"'post' of '{actionRunner.DisplayName}' already push to child post step stack.");
|
Trace.Info($"'post' of '{actionRunner.DisplayName}' already push to child post step stack.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Root.EmbeddedStepsWithPostRegistered[actionRunner.Action.Id] = actionRunner.Condition;
|
Root.EmbeddedStepsWithPostRegistered[actionRunner.Action.Id] = actionRunner.Condition;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -294,7 +298,20 @@ namespace GitHub.Runner.Worker
|
|||||||
Root.PostJobSteps.Push(step);
|
Root.PostJobSteps.Push(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null)
|
public IExecutionContext CreateChild(
|
||||||
|
Guid recordId,
|
||||||
|
string displayName,
|
||||||
|
string refName,
|
||||||
|
string scopeName,
|
||||||
|
string contextName,
|
||||||
|
ActionRunStage stage,
|
||||||
|
Dictionary<string, string> intraActionState = null,
|
||||||
|
int? recordOrder = null,
|
||||||
|
IPagingLogger logger = null,
|
||||||
|
bool isEmbedded = false,
|
||||||
|
CancellationTokenSource cancellationTokenSource = null,
|
||||||
|
Guid embeddedId = default(Guid),
|
||||||
|
string siblingScopeName = null)
|
||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
|
|
||||||
@@ -306,7 +323,6 @@ namespace GitHub.Runner.Worker
|
|||||||
child.Stage = stage;
|
child.Stage = stage;
|
||||||
child.EmbeddedId = embeddedId;
|
child.EmbeddedId = embeddedId;
|
||||||
child.SiblingScopeName = siblingScopeName;
|
child.SiblingScopeName = siblingScopeName;
|
||||||
child.JobTelemetry = JobTelemetry;
|
|
||||||
if (intraActionState == null)
|
if (intraActionState == null)
|
||||||
{
|
{
|
||||||
child.IntraActionState = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
child.IntraActionState = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -346,6 +362,9 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
child.IsEmbedded = isEmbedded;
|
child.IsEmbedded = isEmbedded;
|
||||||
|
child.StepTelemetry.StepId = recordId;
|
||||||
|
child.StepTelemetry.Stage = stage.ToString();
|
||||||
|
child.StepTelemetry.IsEmbedded = isEmbedded;
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
@@ -354,7 +373,13 @@ namespace GitHub.Runner.Worker
|
|||||||
/// An embedded execution context shares the same record ID, record name, logger,
|
/// An embedded execution context shares the same record ID, record name, logger,
|
||||||
/// and a linked cancellation token.
|
/// and a linked cancellation token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IExecutionContext CreateEmbeddedChild(string scopeName, string contextName, Guid embeddedId, ActionRunStage stage, Dictionary<string, string> intraActionState = null, string siblingScopeName = null)
|
public IExecutionContext CreateEmbeddedChild(
|
||||||
|
string scopeName,
|
||||||
|
string contextName,
|
||||||
|
Guid embeddedId,
|
||||||
|
ActionRunStage stage,
|
||||||
|
Dictionary<string, string> intraActionState = null,
|
||||||
|
string siblingScopeName = null)
|
||||||
{
|
{
|
||||||
return Root.CreateChild(_record.Id, _record.Name, _record.Id.ToString("N"), scopeName, contextName, stage, logger: _logger, isEmbedded: true, cancellationTokenSource: CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token), intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName);
|
return Root.CreateChild(_record.Id, _record.Name, _record.Id.ToString("N"), scopeName, contextName, stage, logger: _logger, isEmbedded: true, cancellationTokenSource: CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token), intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName);
|
||||||
}
|
}
|
||||||
@@ -404,6 +429,8 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PublishStepTelemetry();
|
||||||
|
|
||||||
if (Root != this)
|
if (Root != this)
|
||||||
{
|
{
|
||||||
// only dispose TokenSource for step level ExecutionContext
|
// only dispose TokenSource for step level ExecutionContext
|
||||||
@@ -412,14 +439,19 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
_logger.End();
|
_logger.End();
|
||||||
|
|
||||||
|
UpdateGlobalStepsContext();
|
||||||
|
|
||||||
|
return Result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateGlobalStepsContext()
|
||||||
|
{
|
||||||
// Skip if generated context name. Generated context names start with "__". After 3.2 the server will never send an empty context name.
|
// Skip if generated context name. Generated context names start with "__". After 3.2 the server will never send an empty context name.
|
||||||
if (!string.IsNullOrEmpty(ContextName) && !ContextName.StartsWith("__", StringComparison.Ordinal))
|
if (!string.IsNullOrEmpty(ContextName) && !ContextName.StartsWith("__", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
Global.StepsContext.SetOutcome(ScopeName, ContextName, (Outcome ?? Result ?? TaskResult.Succeeded).ToActionResult());
|
Global.StepsContext.SetOutcome(ScopeName, ContextName, (Outcome ?? Result ?? TaskResult.Succeeded).ToActionResult());
|
||||||
Global.StepsContext.SetConclusion(ScopeName, ContextName, (Result ?? TaskResult.Succeeded).ToActionResult());
|
Global.StepsContext.SetConclusion(ScopeName, ContextName, (Result ?? TaskResult.Succeeded).ToActionResult());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetRunnerContext(string name, string value)
|
public void SetRunnerContext(string name, string value)
|
||||||
@@ -519,11 +551,20 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
issue.Message = HostContext.SecretMasker.MaskSecrets(issue.Message);
|
issue.Message = HostContext.SecretMasker.MaskSecrets(issue.Message);
|
||||||
|
if (issue.Message.Length > _maxIssueMessageLength)
|
||||||
|
{
|
||||||
|
issue.Message = issue.Message[.._maxIssueMessageLength];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracking the line number (logFileLineNumber) and step number (stepNumber) for each issue that gets created
|
||||||
|
// Actions UI from the run summary page use both values to easily link to an exact locations in logs where annotations originate from
|
||||||
|
if (_record.Order != null)
|
||||||
|
{
|
||||||
|
issue.Data["stepNumber"] = _record.Order.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
if (issue.Type == IssueType.Error)
|
if (issue.Type == IssueType.Error)
|
||||||
{
|
{
|
||||||
// tracking line number for each issue in log file
|
|
||||||
// log UI use this to navigate from issue to log
|
|
||||||
if (!string.IsNullOrEmpty(logMessage))
|
if (!string.IsNullOrEmpty(logMessage))
|
||||||
{
|
{
|
||||||
long logLineNumber = Write(WellKnownTags.Error, logMessage);
|
long logLineNumber = Write(WellKnownTags.Error, logMessage);
|
||||||
@@ -539,8 +580,6 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
else if (issue.Type == IssueType.Warning)
|
else if (issue.Type == IssueType.Warning)
|
||||||
{
|
{
|
||||||
// tracking line number for each issue in log file
|
|
||||||
// log UI use this to navigate from issue to log
|
|
||||||
if (!string.IsNullOrEmpty(logMessage))
|
if (!string.IsNullOrEmpty(logMessage))
|
||||||
{
|
{
|
||||||
long logLineNumber = Write(WellKnownTags.Warning, logMessage);
|
long logLineNumber = Write(WellKnownTags.Warning, logMessage);
|
||||||
@@ -556,9 +595,6 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
else if (issue.Type == IssueType.Notice)
|
else if (issue.Type == IssueType.Notice)
|
||||||
{
|
{
|
||||||
|
|
||||||
// tracking line number for each issue in log file
|
|
||||||
// log UI use this to navigate from issue to log
|
|
||||||
if (!string.IsNullOrEmpty(logMessage))
|
if (!string.IsNullOrEmpty(logMessage))
|
||||||
{
|
{
|
||||||
long logLineNumber = Write(WellKnownTags.Notice, logMessage);
|
long logLineNumber = Write(WellKnownTags.Notice, logMessage);
|
||||||
@@ -648,22 +684,29 @@ namespace GitHub.Runner.Worker
|
|||||||
// Variables
|
// Variables
|
||||||
Global.Variables = new Variables(HostContext, message.Variables);
|
Global.Variables = new Variables(HostContext, message.Variables);
|
||||||
|
|
||||||
|
if (Global.Variables.GetBoolean("DistributedTask.ForceInternalNodeVersionOnRunnerTo12") ?? false)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable(Constants.Variables.Agent.ForcedInternalNodeVersion, "node12");
|
||||||
|
}
|
||||||
|
|
||||||
// Environment variables shared across all actions
|
// Environment variables shared across all actions
|
||||||
Global.EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer);
|
Global.EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer);
|
||||||
|
|
||||||
// Job defaults shared across all actions
|
// Job defaults shared across all actions
|
||||||
Global.JobDefaults = new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
Global.JobDefaults = new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Job Telemetry
|
||||||
|
Global.JobTelemetry = new List<JobTelemetry>();
|
||||||
|
|
||||||
|
// ActionsStepTelemetry for entire job
|
||||||
|
Global.StepsTelemetry = new List<ActionsStepTelemetry>();
|
||||||
|
|
||||||
// Job Outputs
|
// Job Outputs
|
||||||
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// Actions environment
|
// Actions environment
|
||||||
ActionsEnvironment = message.ActionsEnvironment;
|
ActionsEnvironment = message.ActionsEnvironment;
|
||||||
|
|
||||||
// ActionsStepTelemetry
|
|
||||||
ActionsStepsTelemetry = new List<ActionsStepTelemetry>();
|
|
||||||
|
|
||||||
JobTelemetry = new List<JobTelemetry>();
|
|
||||||
|
|
||||||
// Service container info
|
// Service container info
|
||||||
Global.ServiceContainers = new List<ContainerInfo>();
|
Global.ServiceContainers = new List<ContainerInfo>();
|
||||||
@@ -895,6 +938,83 @@ namespace GitHub.Runner.Worker
|
|||||||
return Root._matchers ?? Array.Empty<IssueMatcherConfig>();
|
return Root._matchers ?? Array.Empty<IssueMatcherConfig>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void PublishStepTelemetry()
|
||||||
|
{
|
||||||
|
if (!_stepTelemetryPublished)
|
||||||
|
{
|
||||||
|
// Add to the global steps telemetry only if we have something to log.
|
||||||
|
if (!string.IsNullOrEmpty(StepTelemetry?.Type))
|
||||||
|
{
|
||||||
|
if (!IsEmbedded)
|
||||||
|
{
|
||||||
|
StepTelemetry.Result = _record.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsEmbedded &&
|
||||||
|
_record.FinishTime != null &&
|
||||||
|
_record.StartTime != null)
|
||||||
|
{
|
||||||
|
StepTelemetry.ExecutionTimeInSeconds = (int)Math.Ceiling((_record.FinishTime - _record.StartTime)?.TotalSeconds ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsEmbedded &&
|
||||||
|
_record.Issues.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var issue in _record.Issues)
|
||||||
|
{
|
||||||
|
if ((issue.Type == IssueType.Error || issue.Type == IssueType.Warning) &&
|
||||||
|
!string.IsNullOrEmpty(issue.Message))
|
||||||
|
{
|
||||||
|
string issueTelemetry;
|
||||||
|
if (issue.Message.Length > _maxIssueMessageLengthInTelemetry)
|
||||||
|
{
|
||||||
|
issueTelemetry = $"{issue.Message[.._maxIssueMessageLengthInTelemetry]}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
issueTelemetry = issue.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
StepTelemetry.ErrorMessages.Add(issueTelemetry);
|
||||||
|
|
||||||
|
// Only send over the first 3 issues to avoid sending too much data.
|
||||||
|
if (StepTelemetry.ErrorMessages.Count >= _maxIssueCountInTelemetry)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Info($"Publish step telemetry for current step {StringUtil.ConvertToJson(StepTelemetry)}.");
|
||||||
|
Global.StepsTelemetry.Add(StepTelemetry);
|
||||||
|
_stepTelemetryPublished = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"Step telemetry has already been published.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteWebhookPayload()
|
||||||
|
{
|
||||||
|
// Makes directory for event_path data
|
||||||
|
var tempDirectory = HostContext.GetDirectory(WellKnownDirectory.Temp);
|
||||||
|
var workflowDirectory = Path.Combine(tempDirectory, "_github_workflow");
|
||||||
|
Directory.CreateDirectory(workflowDirectory);
|
||||||
|
var gitHubEvent = GetGitHubContext("event");
|
||||||
|
|
||||||
|
// adds the GitHub event path/file if the event exists
|
||||||
|
if (gitHubEvent != null)
|
||||||
|
{
|
||||||
|
var workflowFile = Path.Combine(workflowDirectory, "event.json");
|
||||||
|
Trace.Info($"Write event payload to {workflowFile}");
|
||||||
|
File.WriteAllText(workflowFile, gitHubEvent, new UTF8Encoding(false));
|
||||||
|
SetGitHubContext("event_path", workflowFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void InitializeTimelineRecord(Guid timelineId, Guid timelineRecordId, Guid? parentTimelineRecordId, string recordType, string displayName, string refName, int? order)
|
private void InitializeTimelineRecord(Guid timelineId, Guid timelineRecordId, Guid? parentTimelineRecordId, string recordType, string displayName, string refName, int? order)
|
||||||
{
|
{
|
||||||
_mainTimelineId = timelineId;
|
_mainTimelineId = timelineId;
|
||||||
@@ -949,6 +1069,36 @@ namespace GitHub.Runner.Worker
|
|||||||
var newGuid = Guid.NewGuid();
|
var newGuid = Guid.NewGuid();
|
||||||
return CreateChild(newGuid, displayName, newGuid.ToString("N"), null, null, ActionRunStage.Post, intraActionState, _childTimelineRecordOrder - Root.PostJobSteps.Count, siblingScopeName: siblingScopeName);
|
return CreateChild(newGuid, displayName, newGuid.ToString("N"), null, null, ActionRunStage.Post, intraActionState, _childTimelineRecordOrder - Root.PostJobSteps.Count, siblingScopeName: siblingScopeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ApplyContinueOnError(TemplateToken continueOnErrorToken)
|
||||||
|
{
|
||||||
|
if (Result != TaskResult.Failed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var continueOnError = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var templateEvaluator = this.ToPipelineTemplateEvaluator();
|
||||||
|
continueOnError = templateEvaluator.EvaluateStepContinueOnError(continueOnErrorToken, ExpressionValues, ExpressionFunctions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Info("The step failed and an error occurred when attempting to determine whether to continue on error.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
this.Error("The step failed and an error occurred when attempting to determine whether to continue on error.");
|
||||||
|
this.Error(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (continueOnError)
|
||||||
|
{
|
||||||
|
Outcome = Result;
|
||||||
|
Result = TaskResult.Succeeded;
|
||||||
|
Trace.Info($"Updated step result (continue on error)");
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateGlobalStepsContext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Error/Warning/etc methods are created as extension methods to simplify unit testing.
|
// The Error/Warning/etc methods are created as extension methods to simplify unit testing.
|
||||||
@@ -970,7 +1120,6 @@ namespace GitHub.Runner.Worker
|
|||||||
context.Error(ex.Message);
|
context.Error(ex.Message);
|
||||||
context.Debug(ex.ToString());
|
context.Debug(ex.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not add a format string overload. See comment on ExecutionContext.Write().
|
// Do not add a format string overload. See comment on ExecutionContext.Write().
|
||||||
public static void Error(this IExecutionContext context, string message)
|
public static void Error(this IExecutionContext context, string message)
|
||||||
{
|
{
|
||||||
@@ -1044,6 +1193,66 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
return new TemplateTraceWriter(context);
|
return new TemplateTraceWriter(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DictionaryContextData GetExpressionValues(this IExecutionContext context, IStepHost stepHost)
|
||||||
|
{
|
||||||
|
if (stepHost is ContainerStepHost)
|
||||||
|
{
|
||||||
|
|
||||||
|
var expressionValues = context.ExpressionValues.Clone() as DictionaryContextData;
|
||||||
|
context.UpdatePathsInExpressionValues("github", expressionValues, stepHost);
|
||||||
|
context.UpdatePathsInExpressionValues("runner", expressionValues, stepHost);
|
||||||
|
return expressionValues;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return context.ExpressionValues.Clone() as DictionaryContextData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdatePathsInExpressionValues(this IExecutionContext context, string contextName, DictionaryContextData expressionValues, IStepHost stepHost)
|
||||||
|
{
|
||||||
|
var dict = expressionValues[contextName].AssertDictionary($"expected context {contextName} to be a dictionary");
|
||||||
|
context.ResolvePathsInExpressionValuesDictionary(dict, stepHost);
|
||||||
|
expressionValues[contextName] = dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResolvePathsInExpressionValuesDictionary(this IExecutionContext context, DictionaryContextData dict, IStepHost stepHost)
|
||||||
|
{
|
||||||
|
foreach (var key in dict.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (dict[key] is StringContextData)
|
||||||
|
{
|
||||||
|
var value = dict[key].ToString();
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
dict[key] = new StringContextData(stepHost.ResolvePathForStepHost(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (dict[key] is DictionaryContextData)
|
||||||
|
{
|
||||||
|
var innerDict = dict[key].AssertDictionary("expected dictionary");
|
||||||
|
context.ResolvePathsInExpressionValuesDictionary(innerDict, stepHost);
|
||||||
|
var updatedDict = new DictionaryContextData();
|
||||||
|
foreach (var k in innerDict.Keys.ToList())
|
||||||
|
{
|
||||||
|
updatedDict[k] = innerDict[k];
|
||||||
|
}
|
||||||
|
dict[key] = updatedDict;
|
||||||
|
}
|
||||||
|
else if (dict[key] is CaseSensitiveDictionaryContextData)
|
||||||
|
{
|
||||||
|
var innerDict = dict[key].AssertDictionary("expected dictionary");
|
||||||
|
context.ResolvePathsInExpressionValuesDictionary(innerDict, stepHost);
|
||||||
|
var updatedDict = new CaseSensitiveDictionaryContextData();
|
||||||
|
foreach (var k in innerDict.Keys.ToList())
|
||||||
|
{
|
||||||
|
updatedDict[k] = innerDict[k];
|
||||||
|
}
|
||||||
|
dict[key] = updatedDict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class TemplateTraceWriter : ObjectTemplating.ITraceWriter
|
internal sealed class TemplateTraceWriter : ObjectTemplating.ITraceWriter
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using GitHub.Runner.Sdk;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Expressions
|
namespace GitHub.Runner.Worker.Expressions
|
||||||
{
|
{
|
||||||
@@ -62,7 +63,7 @@ namespace GitHub.Runner.Worker.Expressions
|
|||||||
string binDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
string binDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||||
string runnerRoot = new DirectoryInfo(binDir).Parent.FullName;
|
string runnerRoot = new DirectoryInfo(binDir).Parent.FullName;
|
||||||
|
|
||||||
string node = Path.Combine(runnerRoot, "externals", "node12", "bin", $"node{IOUtil.ExeExtension}");
|
string node = Path.Combine(runnerRoot, "externals", NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}");
|
||||||
string hashFilesScript = Path.Combine(binDir, "hashFiles");
|
string hashFilesScript = Path.Combine(binDir, "hashFiles");
|
||||||
var hashResult = string.Empty;
|
var hashResult = string.Empty;
|
||||||
var p = new ProcessInvoker(new HashFilesTrace(context.Trace));
|
var p = new ProcessInvoker(new HashFilesTrace(context.Trace));
|
||||||
|
|||||||
@@ -181,6 +181,10 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
throw new Exception($"Invalid environment variable value. Matching delimiter not found '{delimiter}'");
|
throw new Exception($"Invalid environment variable value. Matching delimiter not found '{delimiter}'");
|
||||||
}
|
}
|
||||||
|
if (newline == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"Invalid environment variable value. EOF marker missing new line.");
|
||||||
|
}
|
||||||
endIndex = index - newline.Length;
|
endIndex = index - newline.Length;
|
||||||
tempLine = ReadLine(text, ref index, out newline);
|
tempLine = ReadLine(text, ref index, out newline);
|
||||||
}
|
}
|
||||||
@@ -259,4 +263,72 @@ namespace GitHub.Runner.Worker
|
|||||||
return text.Substring(originalIndex, lfIndex - originalIndex);
|
return text.Substring(originalIndex, lfIndex - originalIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class CreateStepSummaryCommand : RunnerService, IFileCommandExtension
|
||||||
|
{
|
||||||
|
private const int _attachmentSizeLimit = 128 * 1024;
|
||||||
|
|
||||||
|
public string ContextName => "step_summary";
|
||||||
|
public string FilePrefix => "step_summary_";
|
||||||
|
|
||||||
|
public Type ExtensionType => typeof(IFileCommandExtension);
|
||||||
|
|
||||||
|
public void ProcessCommand(IExecutionContext context, string filePath, ContainerInfo container)
|
||||||
|
{
|
||||||
|
if (!context.Global.Variables.GetBoolean("DistributedTask.UploadStepSummary") ?? true)
|
||||||
|
{
|
||||||
|
Trace.Info("Step Summary is disabled; skipping attachment upload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(filePath) || !File.Exists(filePath))
|
||||||
|
{
|
||||||
|
Trace.Info($"Step Summary file ({filePath}) does not exist; skipping attachment upload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileSize = new FileInfo(filePath).Length;
|
||||||
|
if (fileSize == 0)
|
||||||
|
{
|
||||||
|
Trace.Info($"Step Summary file ({filePath}) is empty; skipping attachment upload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileSize > _attachmentSizeLimit)
|
||||||
|
{
|
||||||
|
context.Error(String.Format(Constants.Runner.UnsupportedSummarySize, _attachmentSizeLimit / 1024, fileSize / 1024));
|
||||||
|
Trace.Info($"Step Summary file ({filePath}) is too large ({fileSize} bytes); skipping attachment upload");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Verbose($"Step Summary file exists: {filePath} and has a file size of {fileSize} bytes");
|
||||||
|
var scrubbedFilePath = filePath + "-scrubbed";
|
||||||
|
|
||||||
|
using (var streamReader = new StreamReader(filePath))
|
||||||
|
using (var streamWriter = new StreamWriter(scrubbedFilePath))
|
||||||
|
{
|
||||||
|
string line;
|
||||||
|
while ((line = streamReader.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
var maskedLine = HostContext.SecretMasker.MaskSecrets(line);
|
||||||
|
streamWriter.WriteLine(maskedLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var attachmentName = context.Id.ToString();
|
||||||
|
|
||||||
|
Trace.Info($"Queueing file ({filePath}) for attachment upload ({attachmentName})");
|
||||||
|
// Attachments must be added to the parent context (job), not the current context (step)
|
||||||
|
context.Root.QueueAttachFile(ChecksAttachmentType.StepSummary, attachmentName, scrubbedFilePath);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Trace.Error($"Error while processing file ({filePath}): {e}");
|
||||||
|
context.Error($"Failed to create step summary using 'GITHUB_STEP_SUMMARY': {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
private readonly HashSet<string> _contextEnvAllowlist = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
private readonly HashSet<string> _contextEnvAllowlist = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"action",
|
|
||||||
"action_path",
|
"action_path",
|
||||||
"action_ref",
|
"action_ref",
|
||||||
"action_repository",
|
"action_repository",
|
||||||
|
"action",
|
||||||
"actor",
|
"actor",
|
||||||
"api_url",
|
"api_url",
|
||||||
"base_ref",
|
"base_ref",
|
||||||
@@ -22,18 +22,20 @@ namespace GitHub.Runner.Worker
|
|||||||
"head_ref",
|
"head_ref",
|
||||||
"job",
|
"job",
|
||||||
"path",
|
"path",
|
||||||
"ref",
|
|
||||||
"ref_name",
|
"ref_name",
|
||||||
"ref_protected",
|
"ref_protected",
|
||||||
"ref_type",
|
"ref_type",
|
||||||
"repository",
|
"ref",
|
||||||
"repository_owner",
|
"repository_owner",
|
||||||
|
"repository",
|
||||||
"retention_days",
|
"retention_days",
|
||||||
"run_attempt",
|
"run_attempt",
|
||||||
"run_id",
|
"run_id",
|
||||||
"run_number",
|
"run_number",
|
||||||
"server_url",
|
"server_url",
|
||||||
"sha",
|
"sha",
|
||||||
|
"step_summary",
|
||||||
|
"triggering_actor",
|
||||||
"workflow",
|
"workflow",
|
||||||
"workspace",
|
"workspace",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ namespace GitHub.Runner.Worker
|
|||||||
public PlanFeatures Features { get; set; }
|
public PlanFeatures Features { get; set; }
|
||||||
public IList<String> FileTable { get; set; }
|
public IList<String> FileTable { get; set; }
|
||||||
public IDictionary<String, IDictionary<String, String>> JobDefaults { get; set; }
|
public IDictionary<String, IDictionary<String, String>> JobDefaults { get; set; }
|
||||||
|
public List<ActionsStepTelemetry> StepsTelemetry { get; set; }
|
||||||
|
public List<JobTelemetry> JobTelemetry { get; set; }
|
||||||
public TaskOrchestrationPlanReference Plan { get; set; }
|
public TaskOrchestrationPlanReference Plan { get; set; }
|
||||||
public List<string> PrependPath { get; set; }
|
public List<string> PrependPath { get; set; }
|
||||||
public List<ContainerInfo> ServiceContainers { get; set; }
|
public List<ContainerInfo> ServiceContainers { get; set; }
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.Expressions2;
|
using GitHub.DistributedTask.Expressions2;
|
||||||
@@ -13,7 +11,6 @@ using GitHub.DistributedTask.WebApi;
|
|||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker;
|
|
||||||
using GitHub.Runner.Worker.Expressions;
|
using GitHub.Runner.Worker.Expressions;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
@@ -42,7 +39,7 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
{
|
{
|
||||||
ArgUtil.NotNull(Data.PreSteps, nameof(Data.PreSteps));
|
ArgUtil.NotNull(Data.PreSteps, nameof(Data.PreSteps));
|
||||||
steps = Data.PreSteps;
|
steps = Data.PreSteps;
|
||||||
}
|
}
|
||||||
else if (stage == ActionRunStage.Post)
|
else if (stage == ActionRunStage.Post)
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(Data.PostSteps, nameof(Data.PostSteps));
|
ArgUtil.NotNull(Data.PostSteps, nameof(Data.PostSteps));
|
||||||
@@ -60,14 +57,14 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
Trace.Info($"Skipping executing post step id: {step.Id}, name: ${step.DisplayName}");
|
Trace.Info($"Skipping executing post step id: {step.Id}, name: ${step.DisplayName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(Data.Steps, nameof(Data.Steps));
|
ArgUtil.NotNull(Data.Steps, nameof(Data.Steps));
|
||||||
steps = Data.Steps;
|
steps = Data.Steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Telemetry to JobContext to send with JobCompleteMessage
|
// Set extra telemetry base on the current context.
|
||||||
if (stage == ActionRunStage.Main)
|
if (stage == ActionRunStage.Main)
|
||||||
{
|
{
|
||||||
var hasRunsStep = false;
|
var hasRunsStep = false;
|
||||||
@@ -83,20 +80,16 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
hasUsesStep = true;
|
hasUsesStep = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var pathReference = Action as Pipelines.RepositoryPathReference;
|
|
||||||
var telemetry = new ActionsStepTelemetry {
|
ExecutionContext.StepTelemetry.HasPreStep = Data.HasPre;
|
||||||
Ref = GetActionRef(),
|
ExecutionContext.StepTelemetry.HasPostStep = Data.HasPost;
|
||||||
HasPreStep = Data.HasPre,
|
|
||||||
HasPostStep = Data.HasPost,
|
ExecutionContext.StepTelemetry.HasRunsStep = hasRunsStep;
|
||||||
IsEmbedded = ExecutionContext.IsEmbedded,
|
ExecutionContext.StepTelemetry.HasUsesStep = hasUsesStep;
|
||||||
Type = "composite",
|
ExecutionContext.StepTelemetry.StepCount = steps.Count;
|
||||||
HasRunsStep = hasRunsStep,
|
|
||||||
HasUsesStep = hasUsesStep,
|
|
||||||
StepCount = steps.Count
|
|
||||||
};
|
|
||||||
ExecutionContext.Root.ActionsStepsTelemetry.Add(telemetry);
|
|
||||||
}
|
}
|
||||||
|
ExecutionContext.StepTelemetry.Type = "composite";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Inputs of the composite step
|
// Inputs of the composite step
|
||||||
@@ -117,7 +110,7 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
// Create embedded steps
|
// Create embedded steps
|
||||||
var embeddedSteps = new List<IStep>();
|
var embeddedSteps = new List<IStep>();
|
||||||
|
|
||||||
// If we need to setup containers beforehand, do it
|
// If we need to setup containers beforehand, do it
|
||||||
// only relevant for local composite actions that need to JIT download/setup containers
|
// only relevant for local composite actions that need to JIT download/setup containers
|
||||||
if (LocalActionContainerSetupSteps != null && LocalActionContainerSetupSteps.Count > 0)
|
if (LocalActionContainerSetupSteps != null && LocalActionContainerSetupSteps.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -152,7 +145,7 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
step.ExecutionContext.ExpressionValues["steps"] = ExecutionContext.Global.StepsContext.GetScope(childScopeName);
|
step.ExecutionContext.ExpressionValues["steps"] = ExecutionContext.Global.StepsContext.GetScope(childScopeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shallow copy github context
|
// Shallow copy github context
|
||||||
@@ -303,13 +296,13 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
// Register job cancellation call back only if job cancellation token not been fire before each step run
|
// Register job cancellation call back only if job cancellation token not been fire before each step run
|
||||||
if (!ExecutionContext.Root.CancellationToken.IsCancellationRequested)
|
if (!ExecutionContext.Root.CancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Test the condition again. The job was canceled after the condition was originally evaluated.
|
// Test the condition again. The job was cancelled after the condition was originally evaluated.
|
||||||
jobCancelRegister = ExecutionContext.Root.CancellationToken.Register(() =>
|
jobCancelRegister = ExecutionContext.Root.CancellationToken.Register(() =>
|
||||||
{
|
{
|
||||||
// Mark job as cancelled
|
// Mark job as cancelled
|
||||||
ExecutionContext.Root.Result = TaskResult.Canceled;
|
ExecutionContext.Root.Result = TaskResult.Canceled;
|
||||||
ExecutionContext.Root.JobContext.Status = ExecutionContext.Root.Result?.ToActionResult();
|
ExecutionContext.Root.JobContext.Status = ExecutionContext.Root.Result?.ToActionResult();
|
||||||
|
|
||||||
step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'.");
|
step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'.");
|
||||||
var conditionReTestTraceWriter = new ConditionTraceWriter(Trace, null); // host tracing only
|
var conditionReTestTraceWriter = new ConditionTraceWriter(Trace, null); // host tracing only
|
||||||
var conditionReTestResult = false;
|
var conditionReTestResult = false;
|
||||||
@@ -378,14 +371,14 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
{
|
{
|
||||||
// Condition is false
|
// Condition is false
|
||||||
Trace.Info("Skipping step due to condition evaluation.");
|
Trace.Info("Skipping step due to condition evaluation.");
|
||||||
step.ExecutionContext.Result = TaskResult.Skipped;
|
SetStepConclusion(step, TaskResult.Skipped);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
else if (conditionEvaluateError != null)
|
else if (conditionEvaluateError != null)
|
||||||
{
|
{
|
||||||
// Condition error
|
// Condition error
|
||||||
step.ExecutionContext.Error(conditionEvaluateError);
|
step.ExecutionContext.Error(conditionEvaluateError);
|
||||||
step.ExecutionContext.Result = TaskResult.Failed;
|
SetStepConclusion(step, TaskResult.Failed);
|
||||||
ExecutionContext.Result = TaskResult.Failed;
|
ExecutionContext.Result = TaskResult.Failed;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -393,7 +386,7 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
{
|
{
|
||||||
await RunStepAsync(step);
|
await RunStepAsync(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -403,13 +396,15 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
jobCancelRegister = null;
|
jobCancelRegister = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Check failed or cancelled
|
||||||
// Check failed or canceled
|
|
||||||
if (step.ExecutionContext.Result == TaskResult.Failed || step.ExecutionContext.Result == TaskResult.Canceled)
|
if (step.ExecutionContext.Result == TaskResult.Failed || step.ExecutionContext.Result == TaskResult.Canceled)
|
||||||
{
|
{
|
||||||
Trace.Info($"Update job result with current composite step result '{step.ExecutionContext.Result}'.");
|
Trace.Info($"Update job result with current composite step result '{step.ExecutionContext.Result}'.");
|
||||||
ExecutionContext.Result = TaskResultUtil.MergeTaskResults(ExecutionContext.Result, step.ExecutionContext.Result.Value);
|
ExecutionContext.Result = TaskResultUtil.MergeTaskResults(ExecutionContext.Result, step.ExecutionContext.Result.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update context
|
||||||
|
step.ExecutionContext.UpdateGlobalStepsContext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,13 +426,13 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
{
|
{
|
||||||
Trace.Error($"Caught timeout exception from step: {ex.Message}");
|
Trace.Error($"Caught timeout exception from step: {ex.Message}");
|
||||||
step.ExecutionContext.Error("The action has timed out.");
|
step.ExecutionContext.Error("The action has timed out.");
|
||||||
step.ExecutionContext.Result = TaskResult.Failed;
|
SetStepConclusion(step, TaskResult.Failed);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Trace.Error($"Caught cancellation exception from step: {ex}");
|
Trace.Error($"Caught cancellation exception from step: {ex}");
|
||||||
step.ExecutionContext.Error(ex);
|
step.ExecutionContext.Error(ex);
|
||||||
step.ExecutionContext.Result = TaskResult.Canceled;
|
SetStepConclusion(step, TaskResult.Canceled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -445,17 +440,26 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
// Log the error and fail the step
|
// Log the error and fail the step
|
||||||
Trace.Error($"Caught exception from step: {ex}");
|
Trace.Error($"Caught exception from step: {ex}");
|
||||||
step.ExecutionContext.Error(ex);
|
step.ExecutionContext.Error(ex);
|
||||||
step.ExecutionContext.Result = TaskResult.Failed;
|
SetStepConclusion(step, TaskResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge execution context result with command result
|
// Merge execution context result with command result
|
||||||
if (step.ExecutionContext.CommandResult != null)
|
if (step.ExecutionContext.CommandResult != null)
|
||||||
{
|
{
|
||||||
step.ExecutionContext.Result = Common.Util.TaskResultUtil.MergeTaskResults(step.ExecutionContext.Result, step.ExecutionContext.CommandResult.Value);
|
SetStepConclusion(step, Common.Util.TaskResultUtil.MergeTaskResults(step.ExecutionContext.Result, step.ExecutionContext.CommandResult.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
step.ExecutionContext.ApplyContinueOnError(step.ContinueOnError);
|
||||||
|
|
||||||
Trace.Info($"Step result: {step.ExecutionContext.Result}");
|
Trace.Info($"Step result: {step.ExecutionContext.Result}");
|
||||||
step.ExecutionContext.Debug($"Finished: {step.DisplayName}");
|
step.ExecutionContext.Debug($"Finished: {step.DisplayName}");
|
||||||
|
step.ExecutionContext.PublishStepTelemetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetStepConclusion(IStep step, TaskResult result)
|
||||||
|
{
|
||||||
|
step.ExecutionContext.Result = result;
|
||||||
|
step.ExecutionContext.UpdateGlobalStepsContext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System;
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.Runner.Worker.Container;
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
{
|
{
|
||||||
@@ -70,18 +70,13 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
}
|
}
|
||||||
|
|
||||||
string type = Action.Type == Pipelines.ActionSourceType.Repository ? "Dockerfile" : "DockerHub";
|
string type = Action.Type == Pipelines.ActionSourceType.Repository ? "Dockerfile" : "DockerHub";
|
||||||
// Add Telemetry to JobContext to send with JobCompleteMessage
|
// Set extra telemetry base on the current context.
|
||||||
if (stage == ActionRunStage.Main)
|
if (stage == ActionRunStage.Main)
|
||||||
{
|
{
|
||||||
var telemetry = new ActionsStepTelemetry {
|
ExecutionContext.StepTelemetry.HasPreStep = Data.HasPre;
|
||||||
Ref = GetActionRef(),
|
ExecutionContext.StepTelemetry.HasPostStep = Data.HasPost;
|
||||||
HasPreStep = Data.HasPre,
|
|
||||||
HasPostStep = Data.HasPost,
|
|
||||||
IsEmbedded = ExecutionContext.IsEmbedded,
|
|
||||||
Type = type
|
|
||||||
};
|
|
||||||
ExecutionContext.Root.ActionsStepsTelemetry.Add(telemetry);
|
|
||||||
}
|
}
|
||||||
|
ExecutionContext.StepTelemetry.Type = type;
|
||||||
|
|
||||||
// run container
|
// run container
|
||||||
var container = new ContainerInfo(HostContext)
|
var container = new ContainerInfo(HostContext)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Linq;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
{
|
{
|
||||||
@@ -22,7 +22,7 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
string ActionDirectory { get; set; }
|
string ActionDirectory { get; set; }
|
||||||
List<JobExtensionRunner> LocalActionContainerSetupSteps { get; set; }
|
List<JobExtensionRunner> LocalActionContainerSetupSteps { get; set; }
|
||||||
Task RunAsync(ActionRunStage stage);
|
Task RunAsync(ActionRunStage stage);
|
||||||
void PrintActionDetails(ActionRunStage stage);
|
void PrepareExecution(ActionRunStage stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class Handler : RunnerService
|
public abstract class Handler : RunnerService
|
||||||
@@ -36,6 +36,7 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
protected IActionCommandManager ActionCommandManager { get; private set; }
|
protected IActionCommandManager ActionCommandManager { get; private set; }
|
||||||
|
|
||||||
public Pipelines.ActionStepDefinitionReference Action { get; set; }
|
public Pipelines.ActionStepDefinitionReference Action { get; set; }
|
||||||
|
public bool IsActionStep => Action != null;
|
||||||
public Dictionary<string, string> Environment { get; set; }
|
public Dictionary<string, string> Environment { get; set; }
|
||||||
public Variables RuntimeVariables { get; set; }
|
public Variables RuntimeVariables { get; set; }
|
||||||
public IExecutionContext ExecutionContext { get; set; }
|
public IExecutionContext ExecutionContext { get; set; }
|
||||||
@@ -44,29 +45,50 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
public string ActionDirectory { get; set; }
|
public string ActionDirectory { get; set; }
|
||||||
public List<JobExtensionRunner> LocalActionContainerSetupSteps { get; set; }
|
public List<JobExtensionRunner> LocalActionContainerSetupSteps { get; set; }
|
||||||
|
|
||||||
public virtual string GetActionRef()
|
public void PrepareExecution(ActionRunStage stage)
|
||||||
{
|
{
|
||||||
if (Action.Type == Pipelines.ActionSourceType.ContainerRegistry)
|
// Print out action details
|
||||||
|
PrintActionDetails(stage);
|
||||||
|
|
||||||
|
// Get telemetry for the action
|
||||||
|
PopulateActionTelemetry(stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void PopulateActionTelemetry(ActionRunStage stage)
|
||||||
|
{
|
||||||
|
if (!IsActionStep)
|
||||||
{
|
{
|
||||||
|
ExecutionContext.StepTelemetry.Type = "runner";
|
||||||
|
ExecutionContext.StepTelemetry.Action = $"{stage} Job Hook";
|
||||||
|
}
|
||||||
|
else if (Action.Type == Pipelines.ActionSourceType.ContainerRegistry)
|
||||||
|
{
|
||||||
|
ExecutionContext.StepTelemetry.Type = "docker";
|
||||||
var registryAction = Action as Pipelines.ContainerRegistryReference;
|
var registryAction = Action as Pipelines.ContainerRegistryReference;
|
||||||
return registryAction.Image;
|
ExecutionContext.StepTelemetry.Action = registryAction.Image;
|
||||||
|
}
|
||||||
|
else if (Action.Type == Pipelines.ActionSourceType.Script)
|
||||||
|
{
|
||||||
|
ExecutionContext.StepTelemetry.Type = "run";
|
||||||
}
|
}
|
||||||
else if (Action.Type == Pipelines.ActionSourceType.Repository)
|
else if (Action.Type == Pipelines.ActionSourceType.Repository)
|
||||||
{
|
{
|
||||||
|
ExecutionContext.StepTelemetry.Type = "repository";
|
||||||
var repoAction = Action as Pipelines.RepositoryPathReference;
|
var repoAction = Action as Pipelines.RepositoryPathReference;
|
||||||
if (string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return repoAction.Path;
|
ExecutionContext.StepTelemetry.Action = repoAction.Path;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
ExecutionContext.StepTelemetry.Ref = repoAction.Ref;
|
||||||
if (string.IsNullOrEmpty(repoAction.Path))
|
if (string.IsNullOrEmpty(repoAction.Path))
|
||||||
{
|
{
|
||||||
return $"{repoAction.Name}@{repoAction.Ref}";
|
ExecutionContext.StepTelemetry.Action = repoAction.Name;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return $"{repoAction.Name}/{repoAction.Path}@{repoAction.Ref}";
|
ExecutionContext.StepTelemetry.Action = $"{repoAction.Name}/{repoAction.Path}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,11 +97,11 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
// this should never happen
|
// this should never happen
|
||||||
Trace.Error($"Can't generate ref for {Action.Type.ToString()}");
|
Trace.Error($"Can't generate ref for {Action.Type.ToString()}");
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
public virtual void PrintActionDetails(ActionRunStage stage)
|
|
||||||
|
protected virtual void PrintActionDetails(ActionRunStage stage)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (stage == ActionRunStage.Post)
|
if (stage == ActionRunStage.Post)
|
||||||
{
|
{
|
||||||
ExecutionContext.Output($"Post job cleanup.");
|
ExecutionContext.Output($"Post job cleanup.");
|
||||||
|
|||||||
@@ -55,7 +55,23 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
else if (data.ExecutionType == ActionExecutionType.NodeJS)
|
else if (data.ExecutionType == ActionExecutionType.NodeJS)
|
||||||
{
|
{
|
||||||
handler = HostContext.CreateService<INodeScriptActionHandler>();
|
handler = HostContext.CreateService<INodeScriptActionHandler>();
|
||||||
(handler as INodeScriptActionHandler).Data = data as NodeJSActionExecutionData;
|
var nodeData = data as NodeJSActionExecutionData;
|
||||||
|
|
||||||
|
// With node12 EoL in 04/2022, we want to be able to uniformly upgrade all JS actions to node16 from the server
|
||||||
|
if (string.Equals(nodeData.NodeVersion, "node12", StringComparison.InvariantCultureIgnoreCase) &&
|
||||||
|
(executionContext.Global.Variables.GetBoolean("DistributedTask.ForceGithubJavascriptActionsToNode16") ?? false))
|
||||||
|
{
|
||||||
|
// The user can opt out of this behaviour by setting this variable to true, either setting 'env' in their workflow or as an environment variable on their machine
|
||||||
|
executionContext.Global.EnvironmentVariables.TryGetValue(Constants.Variables.Actions.AllowActionsUseUnsecureNodeVersion, out var workflowOptOut);
|
||||||
|
var isWorkflowOptOutSet = !string.IsNullOrEmpty(workflowOptOut);
|
||||||
|
var isLocalOptOut = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Actions.AllowActionsUseUnsecureNodeVersion));
|
||||||
|
bool isOptOut = isWorkflowOptOutSet ? StringUtil.ConvertToBoolean(workflowOptOut) : isLocalOptOut;
|
||||||
|
if (!isOptOut)
|
||||||
|
{
|
||||||
|
nodeData.NodeVersion = "node16";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(handler as INodeScriptActionHandler).Data = nodeData;
|
||||||
}
|
}
|
||||||
else if (data.ExecutionType == ActionExecutionType.Script)
|
else if (data.ExecutionType == ActionExecutionType.Script)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
using System.IO;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.Pipelines;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
{
|
{
|
||||||
@@ -74,19 +75,13 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
target = Data.Post;
|
target = Data.Post;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Telemetry to JobContext to send with JobCompleteMessage
|
// Set extra telemetry base on the current context.
|
||||||
if (stage == ActionRunStage.Main)
|
if (stage == ActionRunStage.Main)
|
||||||
{
|
{
|
||||||
var telemetry = new ActionsStepTelemetry
|
ExecutionContext.StepTelemetry.HasPreStep = Data.HasPre;
|
||||||
{
|
ExecutionContext.StepTelemetry.HasPostStep = Data.HasPost;
|
||||||
Ref = GetActionRef(),
|
|
||||||
HasPreStep = Data.HasPre,
|
|
||||||
HasPostStep = Data.HasPost,
|
|
||||||
IsEmbedded = ExecutionContext.IsEmbedded,
|
|
||||||
Type = Data.NodeVersion
|
|
||||||
};
|
|
||||||
ExecutionContext.Root.ActionsStepsTelemetry.Add(telemetry);
|
|
||||||
}
|
}
|
||||||
|
ExecutionContext.StepTelemetry.Type = Data.NodeVersion;
|
||||||
|
|
||||||
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
ArgUtil.NotNullOrEmpty(target, nameof(target));
|
||||||
target = Path.Combine(ActionDirectory, target);
|
target = Path.Combine(ActionDirectory, target);
|
||||||
@@ -119,6 +114,17 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
// Remove environment variable that may cause conflicts with the node within the runner.
|
// Remove environment variable that may cause conflicts with the node within the runner.
|
||||||
Environment.Remove("NODE_ICU_DATA"); // https://github.com/actions/runner/issues/795
|
Environment.Remove("NODE_ICU_DATA"); // https://github.com/actions/runner/issues/795
|
||||||
|
|
||||||
|
if (Data.NodeVersion == "node12" && (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.Node12Warning) ?? false))
|
||||||
|
{
|
||||||
|
if (!ExecutionContext.JobContext.ContainsKey("Node12ActionsWarnings"))
|
||||||
|
{
|
||||||
|
ExecutionContext.JobContext["Node12ActionsWarnings"] = new ArrayContextData();
|
||||||
|
}
|
||||||
|
var repoAction = Action as RepositoryPathReference;
|
||||||
|
var actionDisplayName = new StringContextData(repoAction.Name ?? repoAction.Path); // local actions don't have a 'Name'
|
||||||
|
ExecutionContext.JobContext["Node12ActionsWarnings"].AssertArray("Node12ActionsWarnings").Add(actionDisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
||||||
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -151,6 +151,17 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line.Contains("fatal: unsafe repository", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_executionContext.StepTelemetry.ErrorMessages.Add(line);
|
||||||
|
var gitUnsafeDirNotice = new DTWebApi.Issue
|
||||||
|
{
|
||||||
|
Message = $"You may experience error caused by a recently git safe directory enforcement. For more information see: https://github.blog/changelog/xxx",
|
||||||
|
Type = DTWebApi.IssueType.Notice,
|
||||||
|
};
|
||||||
|
_executionContext.AddIssue(gitUnsafeDirNotice);
|
||||||
|
}
|
||||||
|
|
||||||
// Regular output
|
// Regular output
|
||||||
_executionContext.Output(line);
|
_executionContext.Output(line);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System;
|
||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
@@ -35,6 +35,8 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArgUtil.NotNullOrEmpty(plugin, nameof(plugin));
|
ArgUtil.NotNullOrEmpty(plugin, nameof(plugin));
|
||||||
|
// Set extra telemetry base on the current context.
|
||||||
|
ExecutionContext.StepTelemetry.Type = plugin;
|
||||||
|
|
||||||
// Update the env dictionary.
|
// Update the env dictionary.
|
||||||
AddPrependPathToEnvironment();
|
AddPrependPathToEnvironment();
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Linq;
|
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
using GitHub.Runner.Common;
|
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
@@ -21,18 +22,24 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
{
|
{
|
||||||
public ScriptActionExecutionData Data { get; set; }
|
public ScriptActionExecutionData Data { get; set; }
|
||||||
|
|
||||||
public override void PrintActionDetails(ActionRunStage stage)
|
protected override void PrintActionDetails(ActionRunStage stage)
|
||||||
{
|
{
|
||||||
|
// if we're executing a Job Extension, we won't have an 'Action'
|
||||||
if (stage == ActionRunStage.Post)
|
if (!IsActionStep)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Script action should not have 'Post' job action.");
|
if (Inputs.TryGetValue("path", out var path))
|
||||||
|
{
|
||||||
|
ExecutionContext.Output($"##[group]Run '{path}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Inputs 'path' must be set for job extensions");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else if (Action.Type == Pipelines.ActionSourceType.Script)
|
||||||
Inputs.TryGetValue("script", out string contents);
|
|
||||||
contents = contents ?? string.Empty;
|
|
||||||
if (Action.Type == Pipelines.ActionSourceType.Script)
|
|
||||||
{
|
{
|
||||||
|
Inputs.TryGetValue("script", out string contents);
|
||||||
|
contents = contents ?? string.Empty;
|
||||||
var firstLine = contents.TrimStart(' ', '\t', '\r', '\n');
|
var firstLine = contents.TrimStart(' ', '\t', '\r', '\n');
|
||||||
var firstNewLine = firstLine.IndexOfAny(new[] { '\r', '\n' });
|
var firstNewLine = firstLine.IndexOfAny(new[] { '\r', '\n' });
|
||||||
if (firstNewLine >= 0)
|
if (firstNewLine >= 0)
|
||||||
@@ -41,17 +48,16 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExecutionContext.Output($"##[group]Run {firstLine}");
|
ExecutionContext.Output($"##[group]Run {firstLine}");
|
||||||
|
var multiLines = contents.Replace("\r\n", "\n").TrimEnd('\n').Split('\n');
|
||||||
|
foreach (var line in multiLines)
|
||||||
|
{
|
||||||
|
// Bright Cyan color
|
||||||
|
ExecutionContext.Output($"\x1b[36;1m{line}\x1b[0m");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Invalid action type {Action.Type} for {nameof(ScriptHandler)}");
|
throw new InvalidOperationException($"Invalid action type {Action?.Type} for {nameof(ScriptHandler)}");
|
||||||
}
|
|
||||||
|
|
||||||
var multiLines = contents.Replace("\r\n", "\n").TrimEnd('\n').Split('\n');
|
|
||||||
foreach (var line in multiLines)
|
|
||||||
{
|
|
||||||
// Bright Cyan color
|
|
||||||
ExecutionContext.Output($"\x1b[36;1m{line}\x1b[0m");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string argFormat;
|
string argFormat;
|
||||||
@@ -131,11 +137,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
|
|
||||||
public async Task RunAsync(ActionRunStage stage)
|
public async Task RunAsync(ActionRunStage stage)
|
||||||
{
|
{
|
||||||
if (stage == ActionRunStage.Post)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException("Script action should not have 'Post' job action.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate args
|
// Validate args
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
|
ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
|
||||||
@@ -144,17 +145,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
var githubContext = ExecutionContext.ExpressionValues["github"] as GitHubContext;
|
var githubContext = ExecutionContext.ExpressionValues["github"] as GitHubContext;
|
||||||
ArgUtil.NotNull(githubContext, nameof(githubContext));
|
ArgUtil.NotNull(githubContext, nameof(githubContext));
|
||||||
|
|
||||||
// Add Telemetry to JobContext to send with JobCompleteMessage
|
|
||||||
if (stage == ActionRunStage.Main)
|
|
||||||
{
|
|
||||||
var telemetry = new ActionsStepTelemetry
|
|
||||||
{
|
|
||||||
IsEmbedded = ExecutionContext.IsEmbedded,
|
|
||||||
Type = "run",
|
|
||||||
};
|
|
||||||
ExecutionContext.Root.ActionsStepsTelemetry.Add(telemetry);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempDirectory = HostContext.GetDirectory(WellKnownDirectory.Temp);
|
var tempDirectory = HostContext.GetDirectory(WellKnownDirectory.Temp);
|
||||||
|
|
||||||
Inputs.TryGetValue("script", out var contents);
|
Inputs.TryGetValue("script", out var contents);
|
||||||
@@ -163,7 +153,8 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
string workingDirectory = null;
|
string workingDirectory = null;
|
||||||
if (!Inputs.TryGetValue("workingDirectory", out workingDirectory))
|
if (!Inputs.TryGetValue("workingDirectory", out workingDirectory))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.Global.JobDefaults.TryGetValue("run", out var runDefaults))
|
// Don't use job level working directories for hooks
|
||||||
|
if (IsActionStep && string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.Global.JobDefaults.TryGetValue("run", out var runDefaults))
|
||||||
{
|
{
|
||||||
if (runDefaults.TryGetValue("working-directory", out workingDirectory))
|
if (runDefaults.TryGetValue("working-directory", out workingDirectory))
|
||||||
{
|
{
|
||||||
@@ -222,15 +213,35 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't override runner telemetry here
|
||||||
|
if (!string.IsNullOrEmpty(shellCommand) && IsActionStep)
|
||||||
|
{
|
||||||
|
ExecutionContext.StepTelemetry.Action = shellCommand;
|
||||||
|
}
|
||||||
|
|
||||||
// No arg format was given, shell must be a built-in
|
// No arg format was given, shell must be a built-in
|
||||||
if (string.IsNullOrEmpty(argFormat) || !argFormat.Contains("{0}"))
|
if (string.IsNullOrEmpty(argFormat) || !argFormat.Contains("{0}"))
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Invalid shell option. Shell must be a valid built-in (bash, sh, cmd, powershell, pwsh) or a format string containing '{0}'");
|
throw new ArgumentException("Invalid shell option. Shell must be a valid built-in (bash, sh, cmd, powershell, pwsh) or a format string containing '{0}'");
|
||||||
}
|
}
|
||||||
|
string scriptFilePath, resolvedScriptPath;
|
||||||
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
|
if (IsActionStep)
|
||||||
var scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
|
{
|
||||||
var resolvedScriptPath = $"{StepHost.ResolvePathForStepHost(scriptFilePath).Replace("\"", "\\\"")}";
|
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
|
||||||
|
scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
|
||||||
|
resolvedScriptPath = $"{StepHost.ResolvePathForStepHost(scriptFilePath).Replace("\"", "\\\"")}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// JobExtensionRunners run a script file, we load that from the inputs here
|
||||||
|
if (!Inputs.ContainsKey("path"))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Expected 'path' input to be set");
|
||||||
|
}
|
||||||
|
scriptFilePath = Inputs["path"];
|
||||||
|
ArgUtil.NotNullOrEmpty(scriptFilePath, "path");
|
||||||
|
resolvedScriptPath = Inputs["path"].Replace("\"", "\\\"");
|
||||||
|
}
|
||||||
|
|
||||||
// Format arg string with script path
|
// Format arg string with script path
|
||||||
var arguments = string.Format(argFormat, resolvedScriptPath);
|
var arguments = string.Format(argFormat, resolvedScriptPath);
|
||||||
@@ -246,9 +257,12 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
#else
|
#else
|
||||||
// Don't add a BOM. It causes the script to fail on some operating systems (e.g. on Ubuntu 14).
|
// Don't add a BOM. It causes the script to fail on some operating systems (e.g. on Ubuntu 14).
|
||||||
var encoding = new UTF8Encoding(false);
|
var encoding = new UTF8Encoding(false);
|
||||||
#endif
|
#endif
|
||||||
// Script is written to local path (ie host) but executed relative to the StepHost, which may be a container
|
if (IsActionStep)
|
||||||
File.WriteAllText(scriptFilePath, contents, encoding);
|
{
|
||||||
|
// Script is written to local path (ie host) but executed relative to the StepHost, which may be a container
|
||||||
|
File.WriteAllText(scriptFilePath, contents, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
// Prepend PATH
|
// Prepend PATH
|
||||||
AddPrependPathToEnvironment();
|
AddPrependPathToEnvironment();
|
||||||
@@ -271,10 +285,10 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
if (Environment.ContainsKey("DYLD_INSERT_LIBRARIES")) // We don't check `isContainerStepHost` because we don't support container on macOS
|
if (Environment.ContainsKey("DYLD_INSERT_LIBRARIES")) // We don't check `isContainerStepHost` because we don't support container on macOS
|
||||||
{
|
{
|
||||||
// launch `node macOSRunInvoker.js shell args` instead of `shell args` to avoid macOS SIP remove `DYLD_INSERT_LIBRARIES` when launch process
|
// launch `node macOSRunInvoker.js shell args` instead of `shell args` to avoid macOS SIP remove `DYLD_INSERT_LIBRARIES` when launch process
|
||||||
string node12 = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "node12", "bin", $"node{IOUtil.ExeExtension}");
|
string node = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}");
|
||||||
string macOSRunInvoker = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "macos-run-invoker.js");
|
string macOSRunInvoker = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "macos-run-invoker.js");
|
||||||
arguments = $"\"{macOSRunInvoker.Replace("\"", "\\\"")}\" \"{fileName.Replace("\"", "\\\"")}\" {arguments}";
|
arguments = $"\"{macOSRunInvoker.Replace("\"", "\\\"")}\" \"{fileName.Replace("\"", "\\\"")}\" {arguments}";
|
||||||
fileName = node12;
|
fileName = node;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
var systemConnection = ExecutionContext.Global.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
var systemConnection = ExecutionContext.Global.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
{
|
{
|
||||||
@@ -79,5 +81,22 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
throw new ArgumentException($"Failed to parse COMMAND [..ARGS] from {shellOption}");
|
throw new ArgumentException($"Failed to parse COMMAND [..ARGS] from {shellOption}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static string GetDefaultShellForScript(string path, Common.Tracing trace, string prependPath)
|
||||||
|
{
|
||||||
|
var format = "{0} {1}";
|
||||||
|
switch (Path.GetExtension(path))
|
||||||
|
{
|
||||||
|
case ".sh":
|
||||||
|
// use 'sh' args but prefer bash
|
||||||
|
var pathToShell = WhichUtil.Which("bash", false, trace, prependPath) ?? WhichUtil.Which("sh", true, trace, prependPath);
|
||||||
|
return string.Format(format, pathToShell, _defaultArguments["sh"]);
|
||||||
|
case ".ps1":
|
||||||
|
var pathToPowershell = WhichUtil.Which("pwsh", false, trace, prependPath) ?? WhichUtil.Which("powershell", true, trace, prependPath);
|
||||||
|
return string.Format(format, pathToPowershell, _defaultArguments["powershell"]);
|
||||||
|
default:
|
||||||
|
throw new ArgumentException($"{path} is not a valid path to a script. Make sure it ends in '.sh' or '.ps1'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.Runner.Worker.Container;
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using GitHub.DistributedTask.WebApi;
|
|||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Runner.Worker;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
namespace GitHub.Runner.Worker
|
||||||
@@ -56,6 +57,8 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
// Create a new timeline record for 'Set up job'
|
// Create a new timeline record for 'Set up job'
|
||||||
IExecutionContext context = jobContext.CreateChild(Guid.NewGuid(), "Set up job", $"{nameof(JobExtension)}_Init", null, null, ActionRunStage.Pre);
|
IExecutionContext context = jobContext.CreateChild(Guid.NewGuid(), "Set up job", $"{nameof(JobExtension)}_Init", null, null, ActionRunStage.Pre);
|
||||||
|
context.StepTelemetry.Type = "runner";
|
||||||
|
context.StepTelemetry.Action = "setup_job";
|
||||||
|
|
||||||
List<IStep> preJobSteps = new List<IStep>();
|
List<IStep> preJobSteps = new List<IStep>();
|
||||||
List<IStep> jobSteps = new List<IStep>();
|
List<IStep> jobSteps = new List<IStep>();
|
||||||
@@ -122,20 +125,20 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tokenPermissions = jobContext.Global.Variables.Get("system.github.token.permissions") ?? "";
|
var tokenPermissions = jobContext.Global.Variables.Get("system.github.token.permissions") ?? "";
|
||||||
if (!string.IsNullOrEmpty(tokenPermissions))
|
if (!string.IsNullOrEmpty(tokenPermissions))
|
||||||
{
|
{
|
||||||
context.Output($"##[group]GITHUB_TOKEN Permissions");
|
context.Output($"##[group]GITHUB_TOKEN Permissions");
|
||||||
var permissions = StringUtil.ConvertFromJson<Dictionary<string, string>>(tokenPermissions);
|
var permissions = StringUtil.ConvertFromJson<Dictionary<string, string>>(tokenPermissions);
|
||||||
foreach(KeyValuePair<string, string> entry in permissions)
|
foreach (KeyValuePair<string, string> entry in permissions)
|
||||||
{
|
{
|
||||||
context.Output($"{entry.Key}: {entry.Value}");
|
context.Output($"{entry.Key}: {entry.Value}");
|
||||||
}
|
}
|
||||||
context.Output("##[endgroup]");
|
context.Output("##[endgroup]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
context.Output($"Fail to parse and display GITHUB_TOKEN permissions list: {ex.Message}");
|
context.Output($"Fail to parse and display GITHUB_TOKEN permissions list: {ex.Message}");
|
||||||
@@ -246,6 +249,19 @@ namespace GitHub.Runner.Worker
|
|||||||
Trace.Info("Downloading actions");
|
Trace.Info("Downloading actions");
|
||||||
var actionManager = HostContext.GetService<IActionManager>();
|
var actionManager = HostContext.GetService<IActionManager>();
|
||||||
var prepareResult = await actionManager.PrepareActionsAsync(context, message.Steps);
|
var prepareResult = await actionManager.PrepareActionsAsync(context, message.Steps);
|
||||||
|
|
||||||
|
// add hook to preJobSteps
|
||||||
|
var startedHookPath = Environment.GetEnvironmentVariable("ACTIONS_RUNNER_HOOK_JOB_STARTED");
|
||||||
|
if (!string.IsNullOrEmpty(startedHookPath))
|
||||||
|
{
|
||||||
|
var hookProvider = HostContext.GetService<IJobHookProvider>();
|
||||||
|
var jobHookData = new JobHookData(ActionRunStage.Pre, startedHookPath);
|
||||||
|
preJobSteps.Add(new JobExtensionRunner(runAsync: hookProvider.RunHook,
|
||||||
|
condition: $"{PipelineTemplateConstants.Always}()",
|
||||||
|
displayName: Constants.Hooks.JobStartedStepName,
|
||||||
|
data: (object)jobHookData));
|
||||||
|
}
|
||||||
|
|
||||||
preJobSteps.AddRange(prepareResult.ContainerSetupSteps);
|
preJobSteps.AddRange(prepareResult.ContainerSetupSteps);
|
||||||
|
|
||||||
// Add start-container steps, record and stop-container steps
|
// Add start-container steps, record and stop-container steps
|
||||||
@@ -312,7 +328,9 @@ namespace GitHub.Runner.Worker
|
|||||||
JobExtensionRunner extensionStep = step as JobExtensionRunner;
|
JobExtensionRunner extensionStep = step as JobExtensionRunner;
|
||||||
ArgUtil.NotNull(extensionStep, extensionStep.DisplayName);
|
ArgUtil.NotNull(extensionStep, extensionStep.DisplayName);
|
||||||
Guid stepId = Guid.NewGuid();
|
Guid stepId = Guid.NewGuid();
|
||||||
extensionStep.ExecutionContext = jobContext.CreateChild(stepId, extensionStep.DisplayName, null, null, stepId.ToString("N"), ActionRunStage.Pre);
|
extensionStep.ExecutionContext = jobContext.CreateChild(stepId, extensionStep.DisplayName, stepId.ToString("N"), null, stepId.ToString("N"), ActionRunStage.Pre);
|
||||||
|
extensionStep.ExecutionContext.StepTelemetry.Type = "runner";
|
||||||
|
extensionStep.ExecutionContext.StepTelemetry.Action = extensionStep.DisplayName.ToLowerInvariant().Replace(' ', '_');
|
||||||
}
|
}
|
||||||
else if (step is IActionRunner actionStep)
|
else if (step is IActionRunner actionStep)
|
||||||
{
|
{
|
||||||
@@ -333,6 +351,18 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register Job Completed hook if the variable is set
|
||||||
|
var completedHookPath = Environment.GetEnvironmentVariable("ACTIONS_RUNNER_HOOK_JOB_COMPLETED");
|
||||||
|
if (!string.IsNullOrEmpty(completedHookPath))
|
||||||
|
{
|
||||||
|
var hookProvider = HostContext.GetService<IJobHookProvider>();
|
||||||
|
var jobHookData = new JobHookData(ActionRunStage.Post, completedHookPath);
|
||||||
|
jobContext.RegisterPostJobStep(new JobExtensionRunner(runAsync: hookProvider.RunHook,
|
||||||
|
condition: $"{PipelineTemplateConstants.Always}()",
|
||||||
|
displayName: Constants.Hooks.JobCompletedStepName,
|
||||||
|
data: (object)jobHookData));
|
||||||
|
}
|
||||||
|
|
||||||
List<IStep> steps = new List<IStep>();
|
List<IStep> steps = new List<IStep>();
|
||||||
steps.AddRange(preJobSteps);
|
steps.AddRange(preJobSteps);
|
||||||
steps.AddRange(jobSteps);
|
steps.AddRange(jobSteps);
|
||||||
@@ -401,6 +431,8 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
// create a new timeline record node for 'Finalize job'
|
// create a new timeline record node for 'Finalize job'
|
||||||
IExecutionContext context = jobContext.CreateChild(Guid.NewGuid(), "Complete job", $"{nameof(JobExtension)}_Final", null, null, ActionRunStage.Post);
|
IExecutionContext context = jobContext.CreateChild(Guid.NewGuid(), "Complete job", $"{nameof(JobExtension)}_Final", null, null, ActionRunStage.Post);
|
||||||
|
context.StepTelemetry.Type = "runner";
|
||||||
|
context.StepTelemetry.Action = "complete_job";
|
||||||
using (var register = jobContext.CancellationToken.Register(() => { context.CancelToken(); }))
|
using (var register = jobContext.CancellationToken.Register(() => { context.CancelToken(); }))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
95
src/Runner.Worker/JobHookProvider.cs
Normal file
95
src/Runner.Worker/JobHookProvider.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Linq;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Runner.Worker.Handlers;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Worker
|
||||||
|
{
|
||||||
|
[ServiceLocator(Default = typeof(JobHookProvider))]
|
||||||
|
public interface IJobHookProvider : IRunnerService
|
||||||
|
{
|
||||||
|
Task RunHook(IExecutionContext executionContext, object data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JobHookData
|
||||||
|
{
|
||||||
|
public string Path {get; private set;}
|
||||||
|
public ActionRunStage Stage {get; private set;}
|
||||||
|
|
||||||
|
public JobHookData(ActionRunStage stage, string path)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
Stage = stage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JobHookProvider : RunnerService, IJobHookProvider
|
||||||
|
{
|
||||||
|
public override void Initialize(IHostContext hostContext)
|
||||||
|
{
|
||||||
|
base.Initialize(hostContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunHook(IExecutionContext executionContext, object data)
|
||||||
|
{
|
||||||
|
// Get Inputs
|
||||||
|
var hookData = data as JobHookData;
|
||||||
|
ArgUtil.NotNull(hookData, nameof(JobHookData));
|
||||||
|
|
||||||
|
var displayName = hookData.Stage == ActionRunStage.Pre ? "job started hook" : "job completed hook";
|
||||||
|
// Log to users so that they know how this step was injected
|
||||||
|
executionContext.Output($"A {displayName} has been configured by the self-hosted runner administrator");
|
||||||
|
|
||||||
|
// Validate script file.
|
||||||
|
if (!File.Exists(hookData.Path))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("File doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
executionContext.WriteWebhookPayload();
|
||||||
|
|
||||||
|
// Create the handler data.
|
||||||
|
var scriptDirectory = Path.GetDirectoryName(hookData.Path);
|
||||||
|
var stepHost = HostContext.CreateService<IDefaultStepHost>();
|
||||||
|
var prependPath = string.Join(Path.PathSeparator.ToString(), executionContext.Global.PrependPath.Reverse<string>());
|
||||||
|
Dictionary<string, string> inputs = new()
|
||||||
|
{
|
||||||
|
["path"] = hookData.Path,
|
||||||
|
["shell"] = ScriptHandlerHelpers.GetDefaultShellForScript(hookData.Path, Trace, prependPath)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the handler
|
||||||
|
var handlerFactory = HostContext.GetService<IHandlerFactory>();
|
||||||
|
var handler = handlerFactory.Create(
|
||||||
|
executionContext,
|
||||||
|
action: null,
|
||||||
|
stepHost,
|
||||||
|
new ScriptActionExecutionData(),
|
||||||
|
inputs,
|
||||||
|
environment: new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
|
||||||
|
executionContext.Global.Variables,
|
||||||
|
actionDirectory: scriptDirectory,
|
||||||
|
localActionContainerSetupSteps: null);
|
||||||
|
handler.PrepareExecution(hookData.Stage);
|
||||||
|
|
||||||
|
// Setup file commands
|
||||||
|
var fileCommandManager = HostContext.CreateService<IFileCommandManager>();
|
||||||
|
fileCommandManager.InitializeFiles(executionContext, null);
|
||||||
|
|
||||||
|
// Run the step and process the file commands
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await handler.RunAsync(hookData.Stage);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
fileCommandManager.ProcessFiles(executionContext, executionContext.Global.Container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
using System;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Net.Http;
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using GitHub.Services.WebApi;
|
||||||
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
namespace GitHub.Runner.Worker
|
||||||
{
|
{
|
||||||
@@ -25,6 +26,7 @@ namespace GitHub.Runner.Worker
|
|||||||
public sealed class JobRunner : RunnerService, IJobRunner
|
public sealed class JobRunner : RunnerService, IJobRunner
|
||||||
{
|
{
|
||||||
private IJobServerQueue _jobServerQueue;
|
private IJobServerQueue _jobServerQueue;
|
||||||
|
private RunnerSettings _runnerSettings;
|
||||||
private ITempDirectoryManager _tempDirectoryManager;
|
private ITempDirectoryManager _tempDirectoryManager;
|
||||||
|
|
||||||
public async Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message, CancellationToken jobRequestCancellationToken)
|
public async Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message, CancellationToken jobRequestCancellationToken)
|
||||||
@@ -108,8 +110,8 @@ namespace GitHub.Runner.Worker
|
|||||||
jobContext.SetRunnerContext("os", VarUtil.OS);
|
jobContext.SetRunnerContext("os", VarUtil.OS);
|
||||||
jobContext.SetRunnerContext("arch", VarUtil.OSArchitecture);
|
jobContext.SetRunnerContext("arch", VarUtil.OSArchitecture);
|
||||||
|
|
||||||
var runnerSettings = HostContext.GetService<IConfigurationStore>().GetSettings();
|
_runnerSettings = HostContext.GetService<IConfigurationStore>().GetSettings();
|
||||||
jobContext.SetRunnerContext("name", runnerSettings.AgentName);
|
jobContext.SetRunnerContext("name", _runnerSettings.AgentName);
|
||||||
|
|
||||||
string toolsDirectory = HostContext.GetDirectory(WellKnownDirectory.Tools);
|
string toolsDirectory = HostContext.GetDirectory(WellKnownDirectory.Tools);
|
||||||
Directory.CreateDirectory(toolsDirectory);
|
Directory.CreateDirectory(toolsDirectory);
|
||||||
@@ -130,9 +132,9 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// set the job to canceled
|
// set the job to cancelled
|
||||||
// don't log error issue to job ExecutionContext, since server owns the job level issue
|
// don't log error issue to job ExecutionContext, since server owns the job level issue
|
||||||
Trace.Error($"Job is canceled during initialize.");
|
Trace.Error($"Job is cancelled during initialize.");
|
||||||
Trace.Error($"Caught exception: {ex}");
|
Trace.Error($"Caught exception: {ex}");
|
||||||
return await CompleteJobAsync(jobServer, jobContext, message, TaskResult.Canceled);
|
return await CompleteJobAsync(jobServer, jobContext, message, TaskResult.Canceled);
|
||||||
}
|
}
|
||||||
@@ -209,6 +211,59 @@ namespace GitHub.Runner.Worker
|
|||||||
jobContext.Debug($"Finishing: {message.JobDisplayName}");
|
jobContext.Debug($"Finishing: {message.JobDisplayName}");
|
||||||
TaskResult result = jobContext.Complete(taskResult);
|
TaskResult result = jobContext.Complete(taskResult);
|
||||||
|
|
||||||
|
if (_runnerSettings.DisableUpdate == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentVersion = new PackageVersion(BuildConstants.RunnerPackage.Version);
|
||||||
|
ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
|
VssCredentials serverCredential = VssUtil.GetVssCredential(systemConnection);
|
||||||
|
|
||||||
|
var runnerServer = HostContext.GetService<IRunnerServer>();
|
||||||
|
await runnerServer.ConnectAsync(systemConnection.Url, serverCredential);
|
||||||
|
var serverPackages = await runnerServer.GetPackagesAsync("agent", BuildConstants.RunnerPackage.PackageName, 5, includeToken: false, cancellationToken: CancellationToken.None);
|
||||||
|
if (serverPackages.Count > 0)
|
||||||
|
{
|
||||||
|
serverPackages = serverPackages.OrderByDescending(x => x.Version).ToList();
|
||||||
|
Trace.Info($"Newer packages {StringUtil.ConvertToJson(serverPackages.Select(x => x.Version.ToString()))}");
|
||||||
|
|
||||||
|
var warnOnFailedJob = false; // any minor/patch version behind.
|
||||||
|
var warnOnOldRunnerVersion = false; // >= 2 minor version behind
|
||||||
|
if (serverPackages.Any(x => x.Version.CompareTo(currentVersion) > 0))
|
||||||
|
{
|
||||||
|
Trace.Info($"Current runner version {currentVersion} is behind the latest runner version {serverPackages[0].Version}.");
|
||||||
|
warnOnFailedJob = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverPackages.Where(x => x.Version.Major == currentVersion.Major && x.Version.Minor > currentVersion.Minor).Count() > 1)
|
||||||
|
{
|
||||||
|
Trace.Info($"Current runner version {currentVersion} is way behind the latest runner version {serverPackages[0].Version}.");
|
||||||
|
warnOnOldRunnerVersion = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == TaskResult.Failed && warnOnFailedJob)
|
||||||
|
{
|
||||||
|
jobContext.Warning($"This job failure may be caused by using an out of date self-hosted runner. You are currently using runner version {currentVersion}. Please update to the latest version {serverPackages[0].Version}");
|
||||||
|
}
|
||||||
|
else if (warnOnOldRunnerVersion)
|
||||||
|
{
|
||||||
|
jobContext.Warning($"This self-hosted runner is currently using runner version {currentVersion}. This version is out of date. Please update to the latest version {serverPackages[0].Version}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Ignore any error since suggest runner update is best effort.
|
||||||
|
Trace.Error($"Caught exception during runner version check: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobContext.JobContext.ContainsKey("Node12ActionsWarnings"))
|
||||||
|
{
|
||||||
|
var actions = string.Join(", ", jobContext.JobContext["Node12ActionsWarnings"].AssertArray("Node12ActionsWarnings").Select(action => action.ToString()));
|
||||||
|
jobContext.Warning(string.Format(Constants.Runner.Node12DetectedAfterEndOfLife, actions));
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ShutdownQueue(throwOnFailure: true);
|
await ShutdownQueue(throwOnFailure: true);
|
||||||
@@ -231,14 +286,13 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load any upgrade telemetry
|
// Load any upgrade telemetry
|
||||||
LoadFromTelemetryFile(jobContext.JobTelemetry);
|
LoadFromTelemetryFile(jobContext.Global.JobTelemetry);
|
||||||
|
|
||||||
// Make sure we don't submit secrets as telemetry
|
// Make sure we don't submit secrets as telemetry
|
||||||
MaskTelemetrySecrets(jobContext.JobTelemetry);
|
MaskTelemetrySecrets(jobContext.Global.JobTelemetry);
|
||||||
|
|
||||||
Trace.Info("Raising job completed event.");
|
|
||||||
var jobCompletedEvent = new JobCompletedEvent(message.RequestId, message.JobId, result, jobContext.JobOutputs, jobContext.ActionsEnvironment, jobContext.ActionsStepsTelemetry, jobContext.JobTelemetry);
|
|
||||||
|
|
||||||
|
Trace.Info($"Raising job completed event");
|
||||||
|
var jobCompletedEvent = new JobCompletedEvent(message.RequestId, message.JobId, result, jobContext.JobOutputs, jobContext.ActionsEnvironment, jobContext.Global.StepsTelemetry, jobContext.Global.JobTelemetry);
|
||||||
|
|
||||||
var completeJobRetryLimit = 5;
|
var completeJobRetryLimit = 5;
|
||||||
var exceptions = new List<Exception>();
|
var exceptions = new List<Exception>();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||||
<NoWarn>NU1701;NU1603</NoWarn>
|
<NoWarn>NU1701;NU1603</NoWarn>
|
||||||
<Version>$(Version)</Version>
|
<Version>$(Version)</Version>
|
||||||
|
<PredefinedCulturesOnly>false</PredefinedCulturesOnly>
|
||||||
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
|
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -130,11 +131,13 @@ namespace GitHub.Runner.Worker
|
|||||||
// Register job cancellation call back only if job cancellation token not been fire before each step run
|
// Register job cancellation call back only if job cancellation token not been fire before each step run
|
||||||
if (!jobContext.CancellationToken.IsCancellationRequested)
|
if (!jobContext.CancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Test the condition again. The job was canceled after the condition was originally evaluated.
|
// Test the condition again. The job was cancelled after the condition was originally evaluated.
|
||||||
jobCancelRegister = jobContext.CancellationToken.Register(() =>
|
jobCancelRegister = jobContext.CancellationToken.Register(() =>
|
||||||
{
|
{
|
||||||
// Mark job as cancelled
|
// Mark job as Cancelled or Failed depending on HostContext shutdown token's cancellation
|
||||||
jobContext.Result = TaskResult.Canceled;
|
jobContext.Result = HostContext.RunnerShutdownToken.IsCancellationRequested
|
||||||
|
? TaskResult.Failed
|
||||||
|
: TaskResult.Canceled;
|
||||||
jobContext.JobContext.Status = jobContext.Result?.ToActionResult();
|
jobContext.JobContext.Status = jobContext.Result?.ToActionResult();
|
||||||
|
|
||||||
step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'.");
|
step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'.");
|
||||||
@@ -172,8 +175,10 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
if (jobContext.Result != TaskResult.Canceled)
|
if (jobContext.Result != TaskResult.Canceled)
|
||||||
{
|
{
|
||||||
// Mark job as cancelled
|
// Mark job as Cancelled or Failed depending on HostContext shutdown token's cancellation
|
||||||
jobContext.Result = TaskResult.Canceled;
|
jobContext.Result = HostContext.RunnerShutdownToken.IsCancellationRequested
|
||||||
|
? TaskResult.Failed
|
||||||
|
: TaskResult.Canceled;
|
||||||
jobContext.JobContext.Status = jobContext.Result?.ToActionResult();
|
jobContext.JobContext.Status = jobContext.Result?.ToActionResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,29 +324,8 @@ namespace GitHub.Runner.Worker
|
|||||||
step.ExecutionContext.Result = TaskResultUtil.MergeTaskResults(step.ExecutionContext.Result, step.ExecutionContext.CommandResult.Value);
|
step.ExecutionContext.Result = TaskResultUtil.MergeTaskResults(step.ExecutionContext.Result, step.ExecutionContext.CommandResult.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixup the step result if ContinueOnError
|
step.ExecutionContext.ApplyContinueOnError(step.ContinueOnError);
|
||||||
if (step.ExecutionContext.Result == TaskResult.Failed)
|
|
||||||
{
|
|
||||||
var continueOnError = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
continueOnError = templateEvaluator.EvaluateStepContinueOnError(step.ContinueOnError, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Info("The step failed and an error occurred when attempting to determine whether to continue on error.");
|
|
||||||
Trace.Error(ex);
|
|
||||||
step.ExecutionContext.Error("The step failed and an error occurred when attempting to determine whether to continue on error.");
|
|
||||||
step.ExecutionContext.Error(ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (continueOnError)
|
|
||||||
{
|
|
||||||
step.ExecutionContext.Outcome = step.ExecutionContext.Result;
|
|
||||||
step.ExecutionContext.Result = TaskResult.Succeeded;
|
|
||||||
Trace.Info($"Updated step result (continue on error)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Trace.Info($"Step result: {step.ExecutionContext.Result}");
|
Trace.Info($"Step result: {step.ExecutionContext.Result}");
|
||||||
|
|
||||||
// Complete the step context
|
// Complete the step context
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
namespace GitHub.Runner.Worker
|
||||||
{
|
{
|
||||||
@@ -43,6 +44,7 @@ namespace GitHub.Runner.Worker
|
|||||||
ArgUtil.NotNullOrEmpty(pipeOut, nameof(pipeOut));
|
ArgUtil.NotNullOrEmpty(pipeOut, nameof(pipeOut));
|
||||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
|
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
|
||||||
var jobRunner = HostContext.CreateService<IJobRunner>();
|
var jobRunner = HostContext.CreateService<IJobRunner>();
|
||||||
|
var terminal = HostContext.GetService<ITerminal>();
|
||||||
|
|
||||||
using (var channel = HostContext.CreateService<IProcessChannel>())
|
using (var channel = HostContext.CreateService<IProcessChannel>())
|
||||||
using (var jobRequestCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(HostContext.RunnerShutdownToken))
|
using (var jobRequestCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(HostContext.RunnerShutdownToken))
|
||||||
@@ -64,7 +66,22 @@ namespace GitHub.Runner.Worker
|
|||||||
Trace.Info("Message received.");
|
Trace.Info("Message received.");
|
||||||
ArgUtil.Equal(MessageType.NewJobRequest, channelMessage.MessageType, nameof(channelMessage.MessageType));
|
ArgUtil.Equal(MessageType.NewJobRequest, channelMessage.MessageType, nameof(channelMessage.MessageType));
|
||||||
ArgUtil.NotNullOrEmpty(channelMessage.Body, nameof(channelMessage.Body));
|
ArgUtil.NotNullOrEmpty(channelMessage.Body, nameof(channelMessage.Body));
|
||||||
var jobMessage = StringUtil.ConvertFromJson<Pipelines.AgentJobRequestMessage>(channelMessage.Body);
|
Pipelines.AgentJobRequestMessage jobMessage = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jobMessage = StringUtil.ConvertFromJson<Pipelines.AgentJobRequestMessage>(channelMessage.Body);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException ex)
|
||||||
|
{
|
||||||
|
if (channelMessage.Body.Length > ex.LinePosition + 10)
|
||||||
|
{
|
||||||
|
var errorChunk = channelMessage.Body.Substring(ex.LinePosition - 10, 20);
|
||||||
|
terminal.WriteError($"Worker received invalid Json at position '{ex.LinePosition}': {errorChunk} ({Convert.ToBase64String(Encoding.UTF8.GetBytes(errorChunk))})");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
ArgUtil.NotNull(jobMessage, nameof(jobMessage));
|
ArgUtil.NotNull(jobMessage, nameof(jobMessage));
|
||||||
HostContext.WritePerfCounter($"WorkerJobMessageReceived_{jobMessage.RequestId.ToString()}");
|
HostContext.WritePerfCounter($"WorkerJobMessageReceived_{jobMessage.RequestId.ToString()}");
|
||||||
|
|
||||||
|
|||||||
@@ -129,9 +129,10 @@
|
|||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
"env": "step-env",
|
"env": "step-env",
|
||||||
|
"continue-on-error": "boolean-steps-context",
|
||||||
"working-directory": "string-steps-context",
|
"working-directory": "string-steps-context",
|
||||||
"shell": {
|
"shell": {
|
||||||
"type": "non-empty-string",
|
"type": "string-steps-context",
|
||||||
"required": true
|
"required": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +148,7 @@
|
|||||||
"type": "non-empty-string",
|
"type": "non-empty-string",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
"continue-on-error": "boolean-steps-context",
|
||||||
"with": "step-with",
|
"with": "step-with",
|
||||||
"env": "step-env"
|
"env": "step-env"
|
||||||
}
|
}
|
||||||
@@ -201,6 +203,20 @@
|
|||||||
],
|
],
|
||||||
"string": {}
|
"string": {}
|
||||||
},
|
},
|
||||||
|
"boolean-steps-context": {
|
||||||
|
"context": [
|
||||||
|
"github",
|
||||||
|
"inputs",
|
||||||
|
"strategy",
|
||||||
|
"matrix",
|
||||||
|
"steps",
|
||||||
|
"job",
|
||||||
|
"runner",
|
||||||
|
"env",
|
||||||
|
"hashFiles(1,255)"
|
||||||
|
],
|
||||||
|
"boolean": {}
|
||||||
|
},
|
||||||
"step-env": {
|
"step-env": {
|
||||||
"context": [
|
"context": [
|
||||||
"github",
|
"github",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* ---------------------------------------------------------
|
* ---------------------------------------------------------
|
||||||
* Copyright(C) Microsoft Corporation. All rights reserved.
|
* Copyright(C) Microsoft Corporation. All rights reserved.
|
||||||
* ---------------------------------------------------------
|
* ---------------------------------------------------------
|
||||||
@@ -324,6 +324,7 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
/// <param name="scopeIdentifier">The project GUID to scope the request</param>
|
/// <param name="scopeIdentifier">The project GUID to scope the request</param>
|
||||||
/// <param name="hubName">The name of the server hub: "build" for the Build server or "rm" for the Release Management server</param>
|
/// <param name="hubName">The name of the server hub: "build" for the Build server or "rm" for the Release Management server</param>
|
||||||
/// <param name="planId"></param>
|
/// <param name="planId"></param>
|
||||||
|
/// <param name="jobId"></param>
|
||||||
/// <param name="actionReferenceList"></param>
|
/// <param name="actionReferenceList"></param>
|
||||||
/// <param name="userState"></param>
|
/// <param name="userState"></param>
|
||||||
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
|
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
|
||||||
@@ -331,6 +332,7 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
Guid scopeIdentifier,
|
Guid scopeIdentifier,
|
||||||
string hubName,
|
string hubName,
|
||||||
Guid planId,
|
Guid planId,
|
||||||
|
Guid jobId,
|
||||||
ActionReferenceList actionReferenceList,
|
ActionReferenceList actionReferenceList,
|
||||||
object userState = null,
|
object userState = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -339,12 +341,15 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
Guid locationId = new Guid("27d7f831-88c1-4719-8ca1-6a061dad90eb");
|
Guid locationId = new Guid("27d7f831-88c1-4719-8ca1-6a061dad90eb");
|
||||||
object routeValues = new { scopeIdentifier = scopeIdentifier, hubName = hubName, planId = planId };
|
object routeValues = new { scopeIdentifier = scopeIdentifier, hubName = hubName, planId = planId };
|
||||||
HttpContent content = new ObjectContent<ActionReferenceList>(actionReferenceList, new VssJsonMediaTypeFormatter(true));
|
HttpContent content = new ObjectContent<ActionReferenceList>(actionReferenceList, new VssJsonMediaTypeFormatter(true));
|
||||||
|
|
||||||
|
List<KeyValuePair<string, string>> queryParams = new List<KeyValuePair<string, string>>();
|
||||||
|
queryParams.Add("jobId", jobId);
|
||||||
return SendAsync<ActionDownloadInfoCollection>(
|
return SendAsync<ActionDownloadInfoCollection>(
|
||||||
httpMethod,
|
httpMethod,
|
||||||
locationId,
|
locationId,
|
||||||
routeValues: routeValues,
|
routeValues: routeValues,
|
||||||
version: new ApiResourceVersion(6.0, 1),
|
version: new ApiResourceVersion(6.0, 1),
|
||||||
|
queryParameters: queryParams,
|
||||||
userState: userState,
|
userState: userState,
|
||||||
cancellationToken: cancellationToken,
|
cancellationToken: cancellationToken,
|
||||||
content: content);
|
content: content);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Runtime.Serialization;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.WebApi
|
namespace GitHub.DistributedTask.WebApi
|
||||||
{
|
{
|
||||||
@@ -8,6 +10,13 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
[DataContract]
|
[DataContract]
|
||||||
public class ActionsStepTelemetry
|
public class ActionsStepTelemetry
|
||||||
{
|
{
|
||||||
|
public ActionsStepTelemetry()
|
||||||
|
{
|
||||||
|
this.ErrorMessages = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public string Action { get; set; }
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public string Ref { get; set; }
|
public string Ref { get; set; }
|
||||||
@@ -15,9 +24,15 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public string Stage { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public Guid StepId { get; set; }
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public bool? HasRunsStep { get; set; }
|
public bool? HasRunsStep { get; set; }
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public bool? HasUsesStep { get; set; }
|
public bool? HasUsesStep { get; set; }
|
||||||
|
|
||||||
@@ -32,5 +47,14 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public int? StepCount { get; set; }
|
public int? StepCount { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public TaskResult? Result { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public List<string> ErrorMessages { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public int? ExecutionTimeInSeconds { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
@@ -110,5 +111,43 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A set of trimmed down packages:
|
||||||
|
/// - the package without 'externals'
|
||||||
|
/// - the package without 'dotnet runtime'
|
||||||
|
/// - the package without 'dotnet runtime' and 'externals'
|
||||||
|
/// </summary>
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public List<TrimmedPackageMetadata> TrimmedPackages
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
public class TrimmedPackageMetadata
|
||||||
|
{
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public string HashValue { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public string DownloadUrl { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, string> TrimmedContents
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (m_trimmedContents == null)
|
||||||
|
{
|
||||||
|
m_trimmedContents = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
return m_trimmedContents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataMember(Name = "TrimmedContents", EmitDefaultValue = false)]
|
||||||
|
private Dictionary<string, string> m_trimmedContents;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
this.ProvisioningState = referenceToBeCloned.ProvisioningState;
|
this.ProvisioningState = referenceToBeCloned.ProvisioningState;
|
||||||
this.AccessPoint = referenceToBeCloned.AccessPoint;
|
this.AccessPoint = referenceToBeCloned.AccessPoint;
|
||||||
this.Ephemeral = referenceToBeCloned.Ephemeral;
|
this.Ephemeral = referenceToBeCloned.Ephemeral;
|
||||||
|
this.DisableUpdate = referenceToBeCloned.DisableUpdate;
|
||||||
|
|
||||||
if (referenceToBeCloned.m_links != null)
|
if (referenceToBeCloned.m_links != null)
|
||||||
{
|
{
|
||||||
@@ -92,6 +93,16 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not this agent should auto-update to latest version.
|
||||||
|
/// </summary>
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public bool? DisableUpdate
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not the agent is online.
|
/// Whether or not the agent is online.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -102,4 +102,10 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
public static readonly String FileAttachment = "DistributedTask.Core.FileAttachment";
|
public static readonly String FileAttachment = "DistributedTask.Core.FileAttachment";
|
||||||
public static readonly String DiagnosticLog = "DistributedTask.Core.DiagnosticLog";
|
public static readonly String DiagnosticLog = "DistributedTask.Core.DiagnosticLog";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[GenerateAllConstants]
|
||||||
|
public class ChecksAttachmentType
|
||||||
|
{
|
||||||
|
public static readonly String StepSummary = "Checks.Step.Summary";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -708,33 +708,85 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
|
[InlineData("configure", "once")]
|
||||||
|
[InlineData("remove", "disableupdate")]
|
||||||
|
[InlineData("remove", "ephemeral")]
|
||||||
|
[InlineData("remove", "once")]
|
||||||
|
[InlineData("remove", "replace")]
|
||||||
|
[InlineData("remove", "runasservice")]
|
||||||
|
[InlineData("remove", "unattended")]
|
||||||
|
[InlineData("run", "disableupdate")]
|
||||||
|
[InlineData("run", "ephemeral")]
|
||||||
|
[InlineData("run", "replace")]
|
||||||
|
[InlineData("run", "runasservice")]
|
||||||
|
[InlineData("run", "unattended")]
|
||||||
|
[InlineData("warmup", "disableupdate")]
|
||||||
|
[InlineData("warmup", "ephemeral")]
|
||||||
|
[InlineData("warmup", "once")]
|
||||||
|
[InlineData("warmup", "replace")]
|
||||||
|
[InlineData("warmup", "runasservice")]
|
||||||
|
[InlineData("warmup", "unattended")]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", nameof(CommandSettings))]
|
[Trait("Category", nameof(CommandSettings))]
|
||||||
public void ValidateFlags()
|
public void ValidateInvalidFlagCommandCombination(string validCommand, string flag)
|
||||||
{
|
{
|
||||||
using (TestHostContext hc = CreateTestContext())
|
using (TestHostContext hc = CreateTestContext())
|
||||||
{
|
{
|
||||||
// Arrange.
|
// Arrange.
|
||||||
var command = new CommandSettings(hc, args: new string[] { "--badflag" });
|
var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{flag}" });
|
||||||
|
|
||||||
// Assert.
|
// Assert.
|
||||||
Assert.Contains("badflag", command.Validate());
|
Assert.Contains(flag, command.Validate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
|
[InlineData("remove", "auth", "bar arg value")]
|
||||||
|
[InlineData("remove", "labels", "bar arg value")]
|
||||||
|
[InlineData("remove", "monitorsocketaddress", "bar arg value")]
|
||||||
|
[InlineData("remove", "name", "bar arg value")]
|
||||||
|
[InlineData("remove", "runnergroup", "bar arg value")]
|
||||||
|
[InlineData("remove", "url", "bar arg value")]
|
||||||
|
[InlineData("remove", "username", "bar arg value")]
|
||||||
|
[InlineData("remove", "windowslogonaccount", "bar arg value")]
|
||||||
|
[InlineData("remove", "windowslogonpassword", "bar arg value")]
|
||||||
|
[InlineData("remove", "work", "bar arg value")]
|
||||||
|
[InlineData("run", "auth", "bad arg value")]
|
||||||
|
[InlineData("run", "labels", "bad arg value")]
|
||||||
|
[InlineData("run", "monitorsocketaddress", "bad arg value")]
|
||||||
|
[InlineData("run", "name", "bad arg value")]
|
||||||
|
[InlineData("run", "pat", "bad arg value")]
|
||||||
|
[InlineData("run", "runnergroup", "bad arg value")]
|
||||||
|
[InlineData("run", "token", "bad arg value")]
|
||||||
|
[InlineData("run", "url", "bad arg value")]
|
||||||
|
[InlineData("run", "username", "bad arg value")]
|
||||||
|
[InlineData("run", "windowslogonaccount", "bad arg value")]
|
||||||
|
[InlineData("run", "windowslogonpassword", "bad arg value")]
|
||||||
|
[InlineData("run", "work", "bad arg value")]
|
||||||
|
[InlineData("warmup", "auth", "bad arg value")]
|
||||||
|
[InlineData("warmup", "labels", "bad arg value")]
|
||||||
|
[InlineData("warmup", "monitorsocketaddress", "bad arg value")]
|
||||||
|
[InlineData("warmup", "name", "bad arg value")]
|
||||||
|
[InlineData("warmup", "pat", "bad arg value")]
|
||||||
|
[InlineData("warmup", "runnergroup", "bad arg value")]
|
||||||
|
[InlineData("warmup", "token", "bad arg value")]
|
||||||
|
[InlineData("warmup", "url", "bad arg value")]
|
||||||
|
[InlineData("warmup", "username", "bad arg value")]
|
||||||
|
[InlineData("warmup", "windowslogonaccount", "bad arg value")]
|
||||||
|
[InlineData("warmup", "windowslogonpassword", "bad arg value")]
|
||||||
|
[InlineData("warmup", "work", "bad arg value")]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", nameof(CommandSettings))]
|
[Trait("Category", nameof(CommandSettings))]
|
||||||
public void ValidateArgs()
|
public void ValidateInvalidArgCommandCombination(string validCommand, string arg, string argValue)
|
||||||
{
|
{
|
||||||
using (TestHostContext hc = CreateTestContext())
|
using (TestHostContext hc = CreateTestContext())
|
||||||
{
|
{
|
||||||
// Arrange.
|
// Arrange.
|
||||||
var command = new CommandSettings(hc, args: new string[] { "--badargname", "bad arg value" });
|
var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{arg}", argValue });
|
||||||
|
|
||||||
// Assert.
|
// Assert.
|
||||||
Assert.Contains("badargname", command.Validate());
|
Assert.Contains(arg, command.Validate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -758,6 +810,73 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("configure", "help")]
|
||||||
|
[InlineData("configure", "version")]
|
||||||
|
[InlineData("configure", "commit")]
|
||||||
|
[InlineData("configure", "check")]
|
||||||
|
[InlineData("configure", "disableupdate")]
|
||||||
|
[InlineData("configure", "ephemeral")]
|
||||||
|
[InlineData("configure", "replace")]
|
||||||
|
[InlineData("configure", "runasservice")]
|
||||||
|
[InlineData("configure", "unattended")]
|
||||||
|
[InlineData("remove", "help")]
|
||||||
|
[InlineData("remove", "version")]
|
||||||
|
[InlineData("remove", "commit")]
|
||||||
|
[InlineData("remove", "check")]
|
||||||
|
[InlineData("run", "help")]
|
||||||
|
[InlineData("run", "version")]
|
||||||
|
[InlineData("run", "commit")]
|
||||||
|
[InlineData("run", "check")]
|
||||||
|
[InlineData("run", "once")]
|
||||||
|
[InlineData("warmup", "help")]
|
||||||
|
[InlineData("warmup", "version")]
|
||||||
|
[InlineData("warmup", "commit")]
|
||||||
|
[InlineData("warmup", "check")]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", nameof(CommandSettings))]
|
||||||
|
public void ValidateGoodFlagCommandCombination(string validCommand, string flag)
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
// Arrange.
|
||||||
|
var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{flag}" });
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.True(command.Validate().Count == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("configure", "auth", "good arg value")]
|
||||||
|
[InlineData("configure", "labels", "good arg value")]
|
||||||
|
[InlineData("configure", "monitorsocketaddress", "good arg value")]
|
||||||
|
[InlineData("configure", "name", "good arg value")]
|
||||||
|
[InlineData("configure", "pat", "good arg value")]
|
||||||
|
[InlineData("configure", "runnergroup", "good arg value")]
|
||||||
|
[InlineData("configure", "token", "good arg value")]
|
||||||
|
[InlineData("configure", "url", "good arg value")]
|
||||||
|
[InlineData("configure", "username", "good arg value")]
|
||||||
|
[InlineData("configure", "windowslogonaccount", "good arg value")]
|
||||||
|
[InlineData("configure", "windowslogonpassword", "good arg value")]
|
||||||
|
[InlineData("configure", "work", "good arg value")]
|
||||||
|
[InlineData("remove", "token", "good arg value")]
|
||||||
|
[InlineData("remove", "pat", "good arg value")]
|
||||||
|
[InlineData("run", "startuptype", "good arg value")]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", nameof(CommandSettings))]
|
||||||
|
public void ValidateGoodArgCommandCombination(string validCommand, string arg, string argValue)
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
// Arrange.
|
||||||
|
var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{arg}", argValue });
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.True(command.Validate().Count == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
||||||
{
|
{
|
||||||
TestHostContext hc = new TestHostContext(this, testName);
|
TestHostContext hc = new TestHostContext(this, testName);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
_serviceControlManager = new Mock<ILinuxServiceControlManager>();
|
_serviceControlManager = new Mock<ILinuxServiceControlManager>();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var expectedAgent = new TaskAgent(_expectedAgentName) { Id = 1 };
|
var expectedAgent = new TaskAgent(_expectedAgentName) { Id = 1, Ephemeral = true, DisableUpdate = true };
|
||||||
expectedAgent.Authorization = new TaskAgentAuthorization
|
expectedAgent.Authorization = new TaskAgentAuthorization
|
||||||
{
|
{
|
||||||
ClientId = Guid.NewGuid(),
|
ClientId = Guid.NewGuid(),
|
||||||
@@ -154,7 +154,7 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
tc,
|
tc,
|
||||||
new[]
|
new[]
|
||||||
{
|
{
|
||||||
"configure",
|
"configure",
|
||||||
"--url", _expectedServerUrl,
|
"--url", _expectedServerUrl,
|
||||||
"--name", _expectedAgentName,
|
"--name", _expectedAgentName,
|
||||||
"--runnergroup", _secondRunnerGroupName,
|
"--runnergroup", _secondRunnerGroupName,
|
||||||
@@ -163,6 +163,8 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
"--token", _expectedToken,
|
"--token", _expectedToken,
|
||||||
"--labels", userLabels,
|
"--labels", userLabels,
|
||||||
"--ephemeral",
|
"--ephemeral",
|
||||||
|
"--disableupdate",
|
||||||
|
"--unattended",
|
||||||
});
|
});
|
||||||
trace.Info("Constructed.");
|
trace.Info("Constructed.");
|
||||||
_store.Setup(x => x.IsConfigured()).Returns(false);
|
_store.Setup(x => x.IsConfigured()).Returns(false);
|
||||||
@@ -185,7 +187,7 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
// validate GetAgentPoolsAsync gets called twice with automation pool type
|
// validate GetAgentPoolsAsync gets called twice with automation pool type
|
||||||
_runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2));
|
_runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2));
|
||||||
|
|
||||||
var expectedLabels = new List<string>() { "self-hosted", VarUtil.OS, VarUtil.OSArchitecture};
|
var expectedLabels = new List<string>() { "self-hosted", VarUtil.OS, VarUtil.OSArchitecture };
|
||||||
expectedLabels.AddRange(userLabels.Split(",").ToList());
|
expectedLabels.AddRange(userLabels.Split(",").ToList());
|
||||||
|
|
||||||
_runnerServer.Verify(x => x.AddAgentAsync(It.IsAny<int>(), It.Is<TaskAgent>(a => a.Labels.Select(x => x.Name).ToHashSet().SetEquals(expectedLabels))), Times.Once);
|
_runnerServer.Verify(x => x.AddAgentAsync(It.IsAny<int>(), It.Is<TaskAgent>(a => a.Labels.Select(x => x.Name).ToHashSet().SetEquals(expectedLabels))), Times.Once);
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
using System;
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Services.WebApi;
|
|
||||||
using GitHub.Runner.Listener;
|
|
||||||
using GitHub.Runner.Listener.Configuration;
|
|
||||||
using Moq;
|
|
||||||
using System;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Listener;
|
||||||
|
using GitHub.Runner.Listener.Configuration;
|
||||||
|
using GitHub.Services.Common;
|
||||||
|
using GitHub.Services.WebApi;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests.Listener
|
namespace GitHub.Runner.Common.Tests.Listener
|
||||||
{
|
{
|
||||||
@@ -256,5 +256,67 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
tokenSource.Token), Times.Once());
|
tokenSource.Token), Times.Once());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async void SkipDeleteSession_WhenGetNextMessageGetTaskAgentAccessTokenExpiredException()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
PropertyInfo sessionIdProperty = expectedSession.GetType().GetProperty("SessionId", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
Assert.NotNull(sessionIdProperty);
|
||||||
|
sessionIdProperty.SetValue(expectedSession, Guid.NewGuid());
|
||||||
|
|
||||||
|
_runnerServer
|
||||||
|
.Setup(x => x.CreateAgentSessionAsync(
|
||||||
|
_settings.PoolId,
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
||||||
|
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
||||||
|
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
MessageListener listener = new MessageListener();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
bool result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
Assert.True(result);
|
||||||
|
|
||||||
|
_runnerServer
|
||||||
|
.Setup(x => x.GetAgentMessageAsync(
|
||||||
|
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), tokenSource.Token))
|
||||||
|
.Throws(new TaskAgentAccessTokenExpiredException("test"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
}
|
||||||
|
catch (TaskAgentAccessTokenExpiredException)
|
||||||
|
{
|
||||||
|
//expected
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await listener.DeleteSessionAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
_runnerServer
|
||||||
|
.Verify(x => x.GetAgentMessageAsync(
|
||||||
|
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), tokenSource.Token), Times.Once);
|
||||||
|
|
||||||
|
_runnerServer
|
||||||
|
.Verify(x => x.DeleteAgentSessionAsync(
|
||||||
|
_settings.PoolId, expectedSession.SessionId, It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Listener;
|
using GitHub.Runner.Listener;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using Moq;
|
using Moq;
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests.Listener
|
namespace GitHub.Runner.Common.Tests.Listener
|
||||||
{
|
{
|
||||||
@@ -17,11 +22,12 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
private Mock<IConfigurationStore> _configStore;
|
private Mock<IConfigurationStore> _configStore;
|
||||||
private Mock<IJobDispatcher> _jobDispatcher;
|
private Mock<IJobDispatcher> _jobDispatcher;
|
||||||
private AgentRefreshMessage _refreshMessage = new AgentRefreshMessage(1, "2.299.0");
|
private AgentRefreshMessage _refreshMessage = new AgentRefreshMessage(1, "2.299.0");
|
||||||
|
private List<TrimmedPackageMetadata> _trimmedPackages = new List<TrimmedPackageMetadata>();
|
||||||
|
|
||||||
#if !OS_WINDOWS
|
#if !OS_WINDOWS
|
||||||
private string _packageUrl = $"https://github.com/actions/runner/releases/download/v2.285.1/actions-runner-{BuildConstants.RunnerPackage.PackageName}-2.285.1.tar.gz";
|
private string _packageUrl = null;
|
||||||
#else
|
#else
|
||||||
private string _packageUrl = $"https://github.com/actions/runner/releases/download/v2.285.1/actions-runner-{BuildConstants.RunnerPackage.PackageName}-2.285.1.zip";
|
private string _packageUrl = null;
|
||||||
#endif
|
#endif
|
||||||
public SelfUpdaterL0()
|
public SelfUpdaterL0()
|
||||||
{
|
{
|
||||||
@@ -31,8 +37,44 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
_jobDispatcher = new Mock<IJobDispatcher>();
|
_jobDispatcher = new Mock<IJobDispatcher>();
|
||||||
_configStore.Setup(x => x.GetSettings()).Returns(new RunnerSettings() { PoolId = 1, AgentId = 1 });
|
_configStore.Setup(x => x.GetSettings()).Returns(new RunnerSettings() { PoolId = 1, AgentId = 1 });
|
||||||
|
|
||||||
|
Environment.SetEnvironmentVariable("_GITHUB_ACTION_EXECUTE_UPDATE_SCRIPT", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FetchLatestRunner()
|
||||||
|
{
|
||||||
|
var latestVersion = "";
|
||||||
|
var httpClientHandler = new HttpClientHandler();
|
||||||
|
httpClientHandler.AllowAutoRedirect = false;
|
||||||
|
using (var client = new HttpClient(httpClientHandler))
|
||||||
|
{
|
||||||
|
var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://github.com/actions/runner/releases/latest"));
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.Redirect)
|
||||||
|
{
|
||||||
|
var redirect = await response.Content.ReadAsStringAsync();
|
||||||
|
Regex regex = new Regex(@"/runner/releases/tag/v(?<version>\d+\.\d+\.\d+)");
|
||||||
|
var match = regex.Match(redirect);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
latestVersion = match.Groups["version"].Value;
|
||||||
|
|
||||||
|
#if !OS_WINDOWS
|
||||||
|
_packageUrl = $"https://github.com/actions/runner/releases/download/v{latestVersion}/actions-runner-{BuildConstants.RunnerPackage.PackageName}-{latestVersion}.tar.gz";
|
||||||
|
#else
|
||||||
|
_packageUrl = $"https://github.com/actions/runner/releases/download/v{latestVersion}/actions-runner-{BuildConstants.RunnerPackage.PackageName}-{latestVersion}.zip";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var client = new HttpClient())
|
||||||
|
{
|
||||||
|
var json = await client.GetStringAsync($"https://github.com/actions/runner/releases/download/v{latestVersion}/actions-runner-{BuildConstants.RunnerPackage.PackageName}-{latestVersion}-trimmedpackages.json");
|
||||||
|
_trimmedPackages = StringUtil.ConvertFromJson<List<TrimmedPackageMetadata>>(json);
|
||||||
|
}
|
||||||
|
|
||||||
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
||||||
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl }));
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl }));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -40,40 +82,60 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestSelfUpdateAsync()
|
public async void TestSelfUpdateAsync()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
try
|
||||||
{
|
{
|
||||||
//Arrange
|
await FetchLatestRunner();
|
||||||
var updater = new Runner.Listener.SelfUpdater();
|
Assert.NotNull(_packageUrl);
|
||||||
hc.SetSingleton<ITerminal>(_term.Object);
|
Assert.NotNull(_trimmedPackages);
|
||||||
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
using (var hc = new TestHostContext(this))
|
||||||
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
|
||||||
|
|
||||||
var p = new ProcessInvokerWrapper();
|
|
||||||
p.Initialize(hc);
|
|
||||||
hc.EnqueueInstance<IProcessInvoker>(p);
|
|
||||||
updater.Initialize(hc);
|
|
||||||
|
|
||||||
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
|
||||||
.Callback((int p, int a, string s, string t) =>
|
|
||||||
{
|
|
||||||
hc.GetTrace().Info(t);
|
|
||||||
})
|
|
||||||
.Returns(Task.FromResult(new TaskAgent()));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
hc.GetTrace().Info(_packageUrl);
|
||||||
Assert.True(result);
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0")));
|
|
||||||
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0")));
|
//Arrange
|
||||||
}
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
finally
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
{
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0"), CancellationToken.None);
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0"), CancellationToken.None);
|
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper();
|
||||||
|
p1.Initialize(hc);
|
||||||
|
var p2 = new ProcessInvokerWrapper();
|
||||||
|
p2.Initialize(hc);
|
||||||
|
var p3 = new ProcessInvokerWrapper();
|
||||||
|
p3.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0")));
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0"), CancellationToken.None);
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0"), CancellationToken.None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -81,27 +143,51 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestSelfUpdateAsync_NoUpdateOnOldVersion()
|
public async void TestSelfUpdateAsync_NoUpdateOnOldVersion()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
try
|
||||||
{
|
{
|
||||||
//Arrange
|
await FetchLatestRunner();
|
||||||
var updater = new Runner.Listener.SelfUpdater();
|
Assert.NotNull(_packageUrl);
|
||||||
hc.SetSingleton<ITerminal>(_term.Object);
|
Assert.NotNull(_trimmedPackages);
|
||||||
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
using (var hc = new TestHostContext(this))
|
||||||
updater.Initialize(hc);
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.200.0", true, It.IsAny<CancellationToken>()))
|
//Arrange
|
||||||
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.200.0"), DownloadUrl = _packageUrl }));
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
|
||||||
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
var p1 = new ProcessInvokerWrapper();
|
||||||
.Callback((int p, int a, string s, string t) =>
|
p1.Initialize(hc);
|
||||||
{
|
var p2 = new ProcessInvokerWrapper();
|
||||||
hc.GetTrace().Info(t);
|
p2.Initialize(hc);
|
||||||
})
|
var p3 = new ProcessInvokerWrapper();
|
||||||
.Returns(Task.FromResult(new TaskAgent()));
|
p3.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
var result = await updater.SelfUpdate(new AgentRefreshMessage(1, "2.200.0"), _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.200.0", true, It.IsAny<CancellationToken>()))
|
||||||
Assert.False(result);
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.200.0"), DownloadUrl = _packageUrl }));
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
var result = await updater.SelfUpdate(new AgentRefreshMessage(1, "2.200.0"), _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,33 +196,53 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestSelfUpdateAsync_DownloadRetry()
|
public async void TestSelfUpdateAsync_DownloadRetry()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
try
|
||||||
{
|
{
|
||||||
//Arrange
|
await FetchLatestRunner();
|
||||||
var updater = new Runner.Listener.SelfUpdater();
|
Assert.NotNull(_packageUrl);
|
||||||
hc.SetSingleton<ITerminal>(_term.Object);
|
Assert.NotNull(_trimmedPackages);
|
||||||
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
using (var hc = new TestHostContext(this))
|
||||||
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
//Arrange
|
||||||
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = $"https://github.com/actions/runner/notexists" }));
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
var p = new ProcessInvokerWrapper();
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
||||||
p.Initialize(hc);
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = $"https://github.com/actions/runner/notexists" }));
|
||||||
hc.EnqueueInstance<IProcessInvoker>(p);
|
|
||||||
updater.Initialize(hc);
|
|
||||||
|
|
||||||
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
var p1 = new ProcessInvokerWrapper();
|
||||||
.Callback((int p, int a, string s, string t) =>
|
p1.Initialize(hc);
|
||||||
{
|
var p2 = new ProcessInvokerWrapper();
|
||||||
hc.GetTrace().Info(t);
|
p2.Initialize(hc);
|
||||||
})
|
var p3 = new ProcessInvokerWrapper();
|
||||||
.Returns(Task.FromResult(new TaskAgent()));
|
p3.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<TaskCanceledException>(() => updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken));
|
var ex = await Assert.ThrowsAsync<TaskCanceledException>(() => updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken));
|
||||||
Assert.Contains($"failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts", ex.Message);
|
Assert.Contains($"failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,33 +251,542 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestSelfUpdateAsync_ValidateHash()
|
public async void TestSelfUpdateAsync_ValidateHash()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
try
|
||||||
{
|
{
|
||||||
//Arrange
|
await FetchLatestRunner();
|
||||||
var updater = new Runner.Listener.SelfUpdater();
|
Assert.NotNull(_packageUrl);
|
||||||
hc.SetSingleton<ITerminal>(_term.Object);
|
Assert.NotNull(_trimmedPackages);
|
||||||
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
using (var hc = new TestHostContext(this))
|
||||||
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
//Arrange
|
||||||
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl, HashValue = "bad_hash" }));
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
var p = new ProcessInvokerWrapper();
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
||||||
p.Initialize(hc);
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl, HashValue = "bad_hash" }));
|
||||||
hc.EnqueueInstance<IProcessInvoker>(p);
|
|
||||||
updater.Initialize(hc);
|
|
||||||
|
|
||||||
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
var p1 = new ProcessInvokerWrapper();
|
||||||
.Callback((int p, int a, string s, string t) =>
|
p1.Initialize(hc);
|
||||||
{
|
var p2 = new ProcessInvokerWrapper();
|
||||||
hc.GetTrace().Info(t);
|
p2.Initialize(hc);
|
||||||
})
|
var p3 = new ProcessInvokerWrapper();
|
||||||
.Returns(Task.FromResult(new TaskAgent()));
|
p3.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<Exception>(() => updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken));
|
var ex = await Assert.ThrowsAsync<Exception>(() => updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken));
|
||||||
Assert.Contains("did not match expected Runner Hash", ex.Message);
|
Assert.Contains("did not match expected Runner Hash", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async void TestSelfUpdateAsync_CloneHash_RuntimeAndExternals()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FetchLatestRunner();
|
||||||
|
Assert.NotNull(_packageUrl);
|
||||||
|
Assert.NotNull(_trimmedPackages);
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
|
//Arrange
|
||||||
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper();
|
||||||
|
p1.Initialize(hc);
|
||||||
|
var p2 = new ProcessInvokerWrapper();
|
||||||
|
p2.Initialize(hc);
|
||||||
|
var p3 = new ProcessInvokerWrapper();
|
||||||
|
p3.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl, TrimmedPackages = new List<TrimmedPackageMetadata>() { new TrimmedPackageMetadata() } }));
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0")));
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0")));
|
||||||
|
|
||||||
|
FieldInfo contentHashesProperty = updater.GetType().GetField("_contentHashes", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
Assert.NotNull(contentHashesProperty);
|
||||||
|
Dictionary<string, string> contentHashes = (Dictionary<string, string>)contentHashesProperty.GetValue(updater);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(contentHashes));
|
||||||
|
|
||||||
|
var dotnetRuntimeHashFile = Path.Combine(TestUtil.GetSrcPath(), $"Misc/contentHash/dotnetRuntime/{BuildConstants.RunnerPackage.PackageName}");
|
||||||
|
var externalsHashFile = Path.Combine(TestUtil.GetSrcPath(), $"Misc/contentHash/externals/{BuildConstants.RunnerPackage.PackageName}");
|
||||||
|
|
||||||
|
Assert.Equal(File.ReadAllText(dotnetRuntimeHashFile).Trim(), contentHashes["dotnetRuntime"]);
|
||||||
|
Assert.Equal(File.ReadAllText(externalsHashFile).Trim(), contentHashes["externals"]);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0"), CancellationToken.None);
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0"), CancellationToken.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async void TestSelfUpdateAsync_Cancel_CloneHashTask_WhenNotNeeded()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FetchLatestRunner();
|
||||||
|
Assert.NotNull(_packageUrl);
|
||||||
|
Assert.NotNull(_trimmedPackages);
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
|
//Arrange
|
||||||
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IHttpClientHandlerFactory>(new Mock<IHttpClientHandlerFactory>().Object);
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper();
|
||||||
|
p1.Initialize(hc);
|
||||||
|
var p2 = new ProcessInvokerWrapper();
|
||||||
|
p2.Initialize(hc);
|
||||||
|
var p3 = new ProcessInvokerWrapper();
|
||||||
|
p3.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
||||||
|
|
||||||
|
FieldInfo contentHashesProperty = updater.GetType().GetField("_contentHashes", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
Assert.NotNull(contentHashesProperty);
|
||||||
|
Dictionary<string, string> contentHashes = (Dictionary<string, string>)contentHashesProperty.GetValue(updater);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(contentHashes));
|
||||||
|
|
||||||
|
Assert.NotEqual(2, contentHashes.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
hc.GetTrace().Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async void TestSelfUpdateAsync_UseExternalsTrimmedPackage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FetchLatestRunner();
|
||||||
|
Assert.NotNull(_packageUrl);
|
||||||
|
Assert.NotNull(_trimmedPackages);
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
|
//Arrange
|
||||||
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper(); // hashfiles
|
||||||
|
p1.Initialize(hc);
|
||||||
|
var p2 = new ProcessInvokerWrapper(); // hashfiles
|
||||||
|
p2.Initialize(hc);
|
||||||
|
var p3 = new ProcessInvokerWrapper(); // un-tar
|
||||||
|
p3.Initialize(hc);
|
||||||
|
var p4 = new ProcessInvokerWrapper(); // node -v
|
||||||
|
p4.Initialize(hc);
|
||||||
|
var p5 = new ProcessInvokerWrapper(); // node -v
|
||||||
|
p5.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p4);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p5);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
var trim = _trimmedPackages.Where(x => !x.TrimmedContents.ContainsKey("dotnetRuntime")).ToList();
|
||||||
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl, TrimmedPackages = trim }));
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0")));
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0"), CancellationToken.None);
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0"), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceFile = Path.GetTempFileName();
|
||||||
|
File.Copy(hc.TraceFileName, traceFile, true);
|
||||||
|
|
||||||
|
var externalsHashFile = Path.Combine(TestUtil.GetSrcPath(), $"Misc/contentHash/externals/{BuildConstants.RunnerPackage.PackageName}");
|
||||||
|
var externalsHash = await File.ReadAllTextAsync(externalsHashFile);
|
||||||
|
|
||||||
|
if (externalsHash == trim[0].TrimmedContents["externals"])
|
||||||
|
{
|
||||||
|
Assert.Contains("Use trimmed (externals) package", File.ReadAllText(traceFile));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Contains("the current runner does not carry those trimmed content (Hash mismatch)", File.ReadAllText(traceFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async void TestSelfUpdateAsync_UseExternalsRuntimeTrimmedPackage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FetchLatestRunner();
|
||||||
|
Assert.NotNull(_packageUrl);
|
||||||
|
Assert.NotNull(_trimmedPackages);
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
|
//Arrange
|
||||||
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper(); // hashfiles
|
||||||
|
p1.Initialize(hc);
|
||||||
|
var p2 = new ProcessInvokerWrapper(); // hashfiles
|
||||||
|
p2.Initialize(hc);
|
||||||
|
var p3 = new ProcessInvokerWrapper(); // un-tar
|
||||||
|
p3.Initialize(hc);
|
||||||
|
var p4 = new ProcessInvokerWrapper(); // node -v
|
||||||
|
p4.Initialize(hc);
|
||||||
|
var p5 = new ProcessInvokerWrapper(); // node -v
|
||||||
|
p5.Initialize(hc);
|
||||||
|
var p6 = new ProcessInvokerWrapper(); // runner -v
|
||||||
|
p6.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p4);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p5);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p6);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
var trim = _trimmedPackages.Where(x => x.TrimmedContents.ContainsKey("dotnetRuntime") && x.TrimmedContents.ContainsKey("externals")).ToList();
|
||||||
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl, TrimmedPackages = trim }));
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0")));
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0"), CancellationToken.None);
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0"), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceFile = Path.GetTempFileName();
|
||||||
|
File.Copy(hc.TraceFileName, traceFile, true);
|
||||||
|
|
||||||
|
var externalsHashFile = Path.Combine(TestUtil.GetSrcPath(), $"Misc/contentHash/externals/{BuildConstants.RunnerPackage.PackageName}");
|
||||||
|
var externalsHash = await File.ReadAllTextAsync(externalsHashFile);
|
||||||
|
|
||||||
|
var runtimeHashFile = Path.Combine(TestUtil.GetSrcPath(), $"Misc/contentHash/dotnetRuntime/{BuildConstants.RunnerPackage.PackageName}");
|
||||||
|
var runtimeHash = await File.ReadAllTextAsync(runtimeHashFile);
|
||||||
|
|
||||||
|
if (externalsHash == trim[0].TrimmedContents["externals"] &&
|
||||||
|
runtimeHash == trim[0].TrimmedContents["dotnetRuntime"])
|
||||||
|
{
|
||||||
|
Assert.Contains("Use trimmed (runtime+externals) package", File.ReadAllText(traceFile));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Contains("the current runner does not carry those trimmed content (Hash mismatch)", File.ReadAllText(traceFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async void TestSelfUpdateAsync_NotUseExternalsRuntimeTrimmedPackageOnHashMismatch()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FetchLatestRunner();
|
||||||
|
Assert.NotNull(_packageUrl);
|
||||||
|
Assert.NotNull(_trimmedPackages);
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
|
//Arrange
|
||||||
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper(); // hashfiles
|
||||||
|
p1.Initialize(hc);
|
||||||
|
var p2 = new ProcessInvokerWrapper(); // hashfiles
|
||||||
|
p2.Initialize(hc);
|
||||||
|
var p3 = new ProcessInvokerWrapper(); // un-tar
|
||||||
|
p3.Initialize(hc);
|
||||||
|
var p4 = new ProcessInvokerWrapper(); // node -v
|
||||||
|
p4.Initialize(hc);
|
||||||
|
var p5 = new ProcessInvokerWrapper(); // node -v
|
||||||
|
p5.Initialize(hc);
|
||||||
|
var p6 = new ProcessInvokerWrapper(); // runner -v
|
||||||
|
p6.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p4);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p5);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p6);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
var trim = _trimmedPackages.ToList();
|
||||||
|
foreach (var package in trim)
|
||||||
|
{
|
||||||
|
foreach (var hash in package.TrimmedContents.Keys)
|
||||||
|
{
|
||||||
|
package.TrimmedContents[hash] = "mismatch";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl, TrimmedPackages = trim }));
|
||||||
|
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0")));
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0"), CancellationToken.None);
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0"), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceFile = Path.GetTempFileName();
|
||||||
|
File.Copy(hc.TraceFileName, traceFile, true);
|
||||||
|
Assert.Contains("the current runner does not carry those trimmed content (Hash mismatch)", File.ReadAllText(traceFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async void TestSelfUpdateAsync_FallbackToFullPackage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FetchLatestRunner();
|
||||||
|
Assert.NotNull(_packageUrl);
|
||||||
|
Assert.NotNull(_trimmedPackages);
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), "..", "_layout", "bin")));
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(_packageUrl);
|
||||||
|
hc.GetTrace().Info(StringUtil.ConvertToJson(_trimmedPackages));
|
||||||
|
|
||||||
|
//Arrange
|
||||||
|
var updater = new Runner.Listener.SelfUpdater();
|
||||||
|
hc.SetSingleton<ITerminal>(_term.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper(); // hashfiles
|
||||||
|
p1.Initialize(hc);
|
||||||
|
var p2 = new ProcessInvokerWrapper(); // hashfiles
|
||||||
|
p2.Initialize(hc);
|
||||||
|
var p3 = new ProcessInvokerWrapper(); // un-tar trim
|
||||||
|
p3.Initialize(hc);
|
||||||
|
var p4 = new ProcessInvokerWrapper(); // un-tar full
|
||||||
|
p4.Initialize(hc);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p1);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p2);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p3);
|
||||||
|
hc.EnqueueInstance<IProcessInvoker>(p4);
|
||||||
|
updater.Initialize(hc);
|
||||||
|
|
||||||
|
var trim = _trimmedPackages.ToList();
|
||||||
|
foreach (var package in trim)
|
||||||
|
{
|
||||||
|
package.HashValue = "mismatch";
|
||||||
|
}
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.GetPackageAsync("agent", BuildConstants.RunnerPackage.PackageName, "2.299.0", true, It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(new PackageMetadata() { Platform = BuildConstants.RunnerPackage.PackageName, Version = new PackageVersion("2.299.0"), DownloadUrl = _packageUrl, TrimmedPackages = trim }));
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(1, 1, It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((int p, int a, string s, string t) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info(t);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0")));
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0")));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "bin.2.299.0"), CancellationToken.None);
|
||||||
|
IOUtil.DeleteDirectory(Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "externals.2.299.0"), CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceFile = Path.GetTempFileName();
|
||||||
|
File.Copy(hc.TraceFileName, traceFile, true);
|
||||||
|
if (File.ReadAllText(traceFile).Contains("Use trimmed (runtime+externals) package"))
|
||||||
|
{
|
||||||
|
Assert.Contains("Something wrong with the trimmed runner package, failback to use the full package for runner updates", File.ReadAllText(traceFile));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hc.GetTrace().Warning("Skipping the 'TestSelfUpdateAsync_FallbackToFullPackage' test, as the `externals` or `runtime` hashes have been updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Xunit;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using System.Threading.Channels;
|
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests
|
namespace GitHub.Runner.Common.Tests
|
||||||
{
|
{
|
||||||
@@ -150,5 +150,128 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public async Task RunnerLayoutParts_CheckDotnetRuntimeHash()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
Tracing trace = hc.GetTrace();
|
||||||
|
var dotnetRuntimeHashFile = Path.Combine(TestUtil.GetSrcPath(), $"Misc/contentHash/dotnetRuntime/{BuildConstants.RunnerPackage.PackageName}");
|
||||||
|
trace.Info($"Current hash: {File.ReadAllText(dotnetRuntimeHashFile)}");
|
||||||
|
string layoutTrimsRuntimeAssets = Path.Combine(TestUtil.GetSrcPath(), @"../_layout_trims/runtime");
|
||||||
|
|
||||||
|
string binDir = Path.Combine(TestUtil.GetSrcPath(), @"../_layout/bin");
|
||||||
|
|
||||||
|
#if OS_WINDOWS
|
||||||
|
string node = Path.Combine(TestUtil.GetSrcPath(), @"..\_layout\externals\node12\bin\node");
|
||||||
|
#else
|
||||||
|
string node = Path.Combine(TestUtil.GetSrcPath(), @"../_layout/externals/node12/bin/node");
|
||||||
|
#endif
|
||||||
|
string hashFilesScript = Path.Combine(binDir, "hashFiles");
|
||||||
|
var hashResult = string.Empty;
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper();
|
||||||
|
p1.Initialize(hc);
|
||||||
|
|
||||||
|
p1.ErrorDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data.Data) && data.Data.StartsWith("__OUTPUT__") && data.Data.EndsWith("__OUTPUT__"))
|
||||||
|
{
|
||||||
|
hashResult = data.Data.Substring(10, data.Data.Length - 20);
|
||||||
|
trace.Info($"Hash result: '{hashResult}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace.Info(data.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
p1.OutputDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
trace.Info(data.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
var env = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["patterns"] = "**"
|
||||||
|
};
|
||||||
|
|
||||||
|
int exitCode = await p1.ExecuteAsync(workingDirectory: layoutTrimsRuntimeAssets,
|
||||||
|
fileName: node,
|
||||||
|
arguments: $"\"{hashFilesScript.Replace("\"", "\\\"")}\"",
|
||||||
|
environment: env,
|
||||||
|
requireExitCodeZero: true,
|
||||||
|
outputEncoding: null,
|
||||||
|
killProcessOnCancel: true,
|
||||||
|
cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(string.Equals(hashResult, File.ReadAllText(dotnetRuntimeHashFile).Trim()), $"Hash mismatch for dotnet runtime. You might need to update `Misc/contentHash/dotnetRuntime/{BuildConstants.RunnerPackage.PackageName}` or check if `hashFiles.ts` ever changed recently.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public async Task RunnerLayoutParts_CheckExternalsHash()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
Tracing trace = hc.GetTrace();
|
||||||
|
var externalsHashFile = Path.Combine(TestUtil.GetSrcPath(), $"Misc/contentHash/externals/{BuildConstants.RunnerPackage.PackageName}");
|
||||||
|
trace.Info($"Current hash: {File.ReadAllText(externalsHashFile)}");
|
||||||
|
|
||||||
|
string layoutTrimsExternalsAssets = Path.Combine(TestUtil.GetSrcPath(), @"../_layout_trims/externals");
|
||||||
|
|
||||||
|
string binDir = Path.Combine(TestUtil.GetSrcPath(), @"../_layout/bin");
|
||||||
|
|
||||||
|
#if OS_WINDOWS
|
||||||
|
string node = Path.Combine(TestUtil.GetSrcPath(), @"..\_layout\externals\node12\bin\node");
|
||||||
|
#else
|
||||||
|
string node = Path.Combine(TestUtil.GetSrcPath(), @"../_layout/externals/node12/bin/node");
|
||||||
|
#endif
|
||||||
|
string hashFilesScript = Path.Combine(binDir, "hashFiles");
|
||||||
|
var hashResult = string.Empty;
|
||||||
|
|
||||||
|
var p1 = new ProcessInvokerWrapper();
|
||||||
|
p1.Initialize(hc);
|
||||||
|
|
||||||
|
p1.ErrorDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data.Data) && data.Data.StartsWith("__OUTPUT__") && data.Data.EndsWith("__OUTPUT__"))
|
||||||
|
{
|
||||||
|
hashResult = data.Data.Substring(10, data.Data.Length - 20);
|
||||||
|
trace.Info($"Hash result: '{hashResult}'");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace.Info(data.Data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
p1.OutputDataReceived += (_, data) =>
|
||||||
|
{
|
||||||
|
trace.Info(data.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
var env = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["patterns"] = "**"
|
||||||
|
};
|
||||||
|
|
||||||
|
int exitCode = await p1.ExecuteAsync(workingDirectory: layoutTrimsExternalsAssets,
|
||||||
|
fileName: node,
|
||||||
|
arguments: $"\"{hashFilesScript.Replace("\"", "\\\"")}\"",
|
||||||
|
environment: env,
|
||||||
|
requireExitCodeZero: true,
|
||||||
|
outputEncoding: null,
|
||||||
|
killProcessOnCancel: true,
|
||||||
|
cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(string.Equals(hashResult, File.ReadAllText(externalsHashFile).Trim()), $"Hash mismatch for externals. You might need to update `Misc/contentHash/externals/{BuildConstants.RunnerPackage.PackageName}` or check if `hashFiles.ts` ever changed recently.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Listener.Configuration;
|
using GitHub.Runner.Listener.Configuration;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -15,7 +16,7 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
{
|
{
|
||||||
RunnerSettings settings = new RunnerSettings();
|
RunnerSettings settings = new RunnerSettings();
|
||||||
|
|
||||||
settings.AgentName = "thisiskindofalongrunnername1";
|
settings.AgentName = "thisiskindofalongrunnerabcde";
|
||||||
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
||||||
settings.GitHubUrl = "https://github.com/myorganizationexample/myrepoexample";
|
settings.GitHubUrl = "https://github.com/myorganizationexample/myrepoexample";
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
Assert.Equal("actions", serviceNameParts[0]);
|
Assert.Equal("actions", serviceNameParts[0]);
|
||||||
Assert.Equal("runner", serviceNameParts[1]);
|
Assert.Equal("runner", serviceNameParts[1]);
|
||||||
Assert.Equal("myorganizationexample-myrepoexample", serviceNameParts[2]); // '/' has been replaced with '-'
|
Assert.Equal("myorganizationexample-myrepoexample", serviceNameParts[2]); // '/' has been replaced with '-'
|
||||||
Assert.Equal("thisiskindofalongrunnername1", serviceNameParts[3]);
|
Assert.Equal("thisiskindofalongrunnerabcde", serviceNameParts[3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
{
|
{
|
||||||
RunnerSettings settings = new RunnerSettings();
|
RunnerSettings settings = new RunnerSettings();
|
||||||
|
|
||||||
settings.AgentName = "thisiskindofalongrunnername12";
|
settings.AgentName = "thisiskindofalongrunnernabcde";
|
||||||
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
||||||
settings.GitHubUrl = "https://github.com/myorganizationexample/myrepoexample";
|
settings.GitHubUrl = "https://github.com/myorganizationexample/myrepoexample";
|
||||||
|
|
||||||
@@ -82,88 +83,129 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
Assert.Equal("actions", serviceNameParts[0]);
|
Assert.Equal("actions", serviceNameParts[0]);
|
||||||
Assert.Equal("runner", serviceNameParts[1]);
|
Assert.Equal("runner", serviceNameParts[1]);
|
||||||
Assert.Equal("myorganizationexample-myrepoexample", serviceNameParts[2]); // '/' has been replaced with '-'
|
Assert.Equal("myorganizationexample-myrepoexample", serviceNameParts[2]); // '/' has been replaced with '-'
|
||||||
Assert.Equal("thisiskindofalongrunnername12", serviceNameParts[3]);
|
Assert.Equal("thisiskindofalongrunnernabcde", serviceNameParts[3]); // should not have random numbers unless we exceed 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
#if OS_WINDOWS
|
||||||
[Trait("Level", "L0")]
|
[Fact]
|
||||||
[Trait("Category", "Service")]
|
[Trait("Level", "L0")]
|
||||||
public void CalculateServiceNameLimitsServiceNameTo80Chars()
|
[Trait("Category", "Service")]
|
||||||
{
|
public void CalculateServiceNameLimitsServiceNameTo80Chars()
|
||||||
RunnerSettings settings = new RunnerSettings();
|
|
||||||
|
|
||||||
settings.AgentName = "thisisareallyreallylongbutstillvalidagentname";
|
|
||||||
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
|
||||||
settings.GitHubUrl = "https://github.com/myreallylongorganizationexample/myreallylongrepoexample";
|
|
||||||
|
|
||||||
string serviceNamePattern = "actions.runner.{0}.{1}";
|
|
||||||
string serviceDisplayNamePattern = "GitHub Actions Runner ({0}.{1})";
|
|
||||||
|
|
||||||
using (TestHostContext hc = CreateTestContext())
|
|
||||||
{
|
{
|
||||||
ServiceControlManager scm = new ServiceControlManager();
|
RunnerSettings settings = new RunnerSettings();
|
||||||
|
|
||||||
scm.Initialize(hc);
|
settings.AgentName = "thisisareallyreallylongbutstillvalidagentname";
|
||||||
scm.CalculateServiceName(
|
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
||||||
settings,
|
settings.GitHubUrl = "https://github.com/myreallylongorganizationexample/myreallylongrepoexample";
|
||||||
serviceNamePattern,
|
|
||||||
serviceDisplayNamePattern,
|
|
||||||
out string serviceName,
|
|
||||||
out string serviceDisplayName);
|
|
||||||
|
|
||||||
// Verify name has been shortened to 80 characters
|
string serviceNamePattern = "actions.runner.{0}.{1}";
|
||||||
Assert.Equal(80, serviceName.Length);
|
string serviceDisplayNamePattern = "GitHub Actions Runner ({0}.{1})";
|
||||||
|
|
||||||
var serviceNameParts = serviceName.Split('.');
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
ServiceControlManager scm = new ServiceControlManager();
|
||||||
|
|
||||||
// Verify that each component has been shortened to a sensible length
|
scm.Initialize(hc);
|
||||||
Assert.Equal("actions", serviceNameParts[0]); // Never shortened
|
scm.CalculateServiceName(
|
||||||
Assert.Equal("runner", serviceNameParts[1]); // Never shortened
|
settings,
|
||||||
Assert.Equal("myreallylongorganizationexample-myreallylongr", serviceNameParts[2]); // First 45 chars, '/' has been replaced with '-'
|
serviceNamePattern,
|
||||||
Assert.Equal("thisisareallyreally", serviceNameParts[3]); // Remainder of unused chars
|
serviceDisplayNamePattern,
|
||||||
|
out string serviceName,
|
||||||
|
out string serviceDisplayName);
|
||||||
|
|
||||||
|
// Verify name has been shortened to 80 characters
|
||||||
|
Assert.Equal(80, serviceName.Length);
|
||||||
|
|
||||||
|
var serviceNameParts = serviceName.Split('.');
|
||||||
|
|
||||||
|
// Verify that each component has been shortened to a sensible length
|
||||||
|
Assert.Equal("actions", serviceNameParts[0]); // Never shortened
|
||||||
|
Assert.Equal("runner", serviceNameParts[1]); // Never shortened
|
||||||
|
Assert.Equal("myreallylongorganizationexample-myreallylongr", serviceNameParts[2]); // First 45 chars, '/' has been replaced with '-'
|
||||||
|
Assert.Matches(@"^(thisisareallyr-[0-9]{4})$", serviceNameParts[3]); // Remainder of unused chars, 4 random numbers added at the end
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Special 'defensive' test that verifies we can gracefully handle creating service names
|
// Special 'defensive' test that verifies we can gracefully handle creating service names
|
||||||
// in case GitHub.com changes its org/repo naming convention in the future,
|
// in case GitHub.com changes its org/repo naming convention in the future,
|
||||||
// and some of these characters may be invalid for service names
|
// and some of these characters may be invalid for service names
|
||||||
// Not meant to test character set exhaustively -- it's just here to exercise the sanitizing logic
|
// Not meant to test character set exhaustively -- it's just here to exercise the sanitizing logic
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Service")]
|
[Trait("Category", "Service")]
|
||||||
public void CalculateServiceNameSanitizeOutOfRangeChars()
|
public void CalculateServiceNameSanitizeOutOfRangeChars()
|
||||||
{
|
|
||||||
RunnerSettings settings = new RunnerSettings();
|
|
||||||
|
|
||||||
settings.AgentName = "name";
|
|
||||||
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
|
||||||
settings.GitHubUrl = "https://github.com/org!@$*+[]()/repo!@$*+[]()";
|
|
||||||
|
|
||||||
string serviceNamePattern = "actions.runner.{0}.{1}";
|
|
||||||
string serviceDisplayNamePattern = "GitHub Actions Runner ({0}.{1})";
|
|
||||||
|
|
||||||
using (TestHostContext hc = CreateTestContext())
|
|
||||||
{
|
{
|
||||||
ServiceControlManager scm = new ServiceControlManager();
|
RunnerSettings settings = new RunnerSettings();
|
||||||
|
|
||||||
scm.Initialize(hc);
|
settings.AgentName = "name";
|
||||||
scm.CalculateServiceName(
|
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
||||||
settings,
|
settings.GitHubUrl = "https://github.com/org!@$*+[]()/repo!@$*+[]()";
|
||||||
serviceNamePattern,
|
|
||||||
serviceDisplayNamePattern,
|
|
||||||
out string serviceName,
|
|
||||||
out string serviceDisplayName);
|
|
||||||
|
|
||||||
var serviceNameParts = serviceName.Split('.');
|
string serviceNamePattern = "actions.runner.{0}.{1}";
|
||||||
|
string serviceDisplayNamePattern = "GitHub Actions Runner ({0}.{1})";
|
||||||
|
|
||||||
// Verify service name parts are sanitized correctly
|
using (TestHostContext hc = CreateTestContext())
|
||||||
Assert.Equal("actions", serviceNameParts[0]);
|
{
|
||||||
Assert.Equal("runner", serviceNameParts[1]);
|
ServiceControlManager scm = new ServiceControlManager();
|
||||||
Assert.Equal("org----------repo---------", serviceNameParts[2]); // Chars replaced with '-'
|
|
||||||
Assert.Equal("name", serviceNameParts[3]);
|
scm.Initialize(hc);
|
||||||
|
scm.CalculateServiceName(
|
||||||
|
settings,
|
||||||
|
serviceNamePattern,
|
||||||
|
serviceDisplayNamePattern,
|
||||||
|
out string serviceName,
|
||||||
|
out string serviceDisplayName);
|
||||||
|
|
||||||
|
var serviceNameParts = serviceName.Split('.');
|
||||||
|
|
||||||
|
// Verify service name parts are sanitized correctly
|
||||||
|
Assert.Equal("actions", serviceNameParts[0]);
|
||||||
|
Assert.Equal("runner", serviceNameParts[1]);
|
||||||
|
Assert.Equal("org----------repo---------", serviceNameParts[2]); // Chars replaced with '-'
|
||||||
|
Assert.Equal("name", serviceNameParts[3]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
#else
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Service")]
|
||||||
|
public void CalculateServiceNameLimitsServiceNameTo150Chars()
|
||||||
|
{
|
||||||
|
RunnerSettings settings = new RunnerSettings();
|
||||||
|
|
||||||
|
settings.AgentName = "thisisareallyreallylongbutstillvalidagentnameiamusingforthisexampletotestverylongnamelimits";
|
||||||
|
settings.ServerUrl = "https://example.githubusercontent.com/12345678901234567890123456789012345678901234567890";
|
||||||
|
settings.GitHubUrl = "https://github.com/myreallylongorganizationexampleonlinux/myreallylongrepoexampleonlinux1234";
|
||||||
|
|
||||||
|
string serviceNamePattern = "actions.runner.{0}.{1}";
|
||||||
|
string serviceDisplayNamePattern = "GitHub Actions Runner ({0}.{1})";
|
||||||
|
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
ServiceControlManager scm = new ServiceControlManager();
|
||||||
|
|
||||||
|
scm.Initialize(hc);
|
||||||
|
scm.CalculateServiceName(
|
||||||
|
settings,
|
||||||
|
serviceNamePattern,
|
||||||
|
serviceDisplayNamePattern,
|
||||||
|
out string serviceName,
|
||||||
|
out string serviceDisplayName);
|
||||||
|
|
||||||
|
// Verify name has been shortened to 150
|
||||||
|
Assert.Equal(150, serviceName.Length);
|
||||||
|
|
||||||
|
var serviceNameParts = serviceName.Split('.');
|
||||||
|
|
||||||
|
// Verify that each component has been shortened to a sensible length
|
||||||
|
Assert.Equal("actions", serviceNameParts[0]); // Never shortened
|
||||||
|
Assert.Equal("runner", serviceNameParts[1]); // Never shortened
|
||||||
|
Assert.Equal("myreallylongorganizationexampleonlinux-myreallylongrepoexampleonlinux1", serviceNameParts[2]); // First 70 chars, '/' has been replaced with '-'
|
||||||
|
Assert.Matches(@"^(thisisareallyreallylongbutstillvalidagentnameiamusingforthi-[0-9]{4})$", serviceNameParts[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -165,7 +165,15 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
switch (directory)
|
switch (directory)
|
||||||
{
|
{
|
||||||
case WellKnownDirectory.Bin:
|
case WellKnownDirectory.Bin:
|
||||||
path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
var overwriteBinDir = Environment.GetEnvironmentVariable("RUNNER_L0_OVERRIDEBINDIR");
|
||||||
|
if (Directory.Exists(overwriteBinDir))
|
||||||
|
{
|
||||||
|
path = overwriteBinDir;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WellKnownDirectory.Diag:
|
case WellKnownDirectory.Diag:
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
using (TestHostContext hc = CreateTestContext())
|
using (TestHostContext hc = CreateTestContext())
|
||||||
{
|
{
|
||||||
_ec.Object.Global.EnvironmentVariables = new Dictionary<string, string>();
|
_ec.Object.Global.EnvironmentVariables = new Dictionary<string, string>();
|
||||||
|
_ec.Object.Global.JobTelemetry = new List<JobTelemetry>();
|
||||||
var expressionValues = new DictionaryContextData
|
var expressionValues = new DictionaryContextData
|
||||||
{
|
{
|
||||||
["env"] =
|
["env"] =
|
||||||
@@ -131,7 +132,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
_ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
|
_ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
|
||||||
_ec.Setup(x => x.JobTelemetry).Returns(new List<JobTelemetry>());
|
|
||||||
|
|
||||||
Assert.True(_commandManager.TryProcessCommand(_ec.Object, $"::stop-commands::{invalidToken}", null));
|
Assert.True(_commandManager.TryProcessCommand(_ec.Object, $"::stop-commands::{invalidToken}", null));
|
||||||
}
|
}
|
||||||
@@ -148,8 +148,8 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
using (TestHostContext hc = CreateTestContext())
|
using (TestHostContext hc = CreateTestContext())
|
||||||
{
|
{
|
||||||
_ec.Object.Global.EnvironmentVariables = new Dictionary<string, string>();
|
_ec.Object.Global.EnvironmentVariables = new Dictionary<string, string>();
|
||||||
|
_ec.Object.Global.JobTelemetry = new List<JobTelemetry>();
|
||||||
_ec.Setup(x => x.ExpressionValues).Returns(GetExpressionValues());
|
_ec.Setup(x => x.ExpressionValues).Returns(GetExpressionValues());
|
||||||
_ec.Setup(x => x.JobTelemetry).Returns(new List<JobTelemetry>());
|
|
||||||
Assert.Throws<Exception>(() => _commandManager.TryProcessCommand(_ec.Object, $"::stop-commands::{invalidToken}", null));
|
Assert.Throws<Exception>(() => _commandManager.TryProcessCommand(_ec.Object, $"::stop-commands::{invalidToken}", null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
@@ -2135,6 +2135,7 @@ runs:
|
|||||||
_ec = new Mock<IExecutionContext>();
|
_ec = new Mock<IExecutionContext>();
|
||||||
_ec.Setup(x => x.Global).Returns(new GlobalContext());
|
_ec.Setup(x => x.Global).Returns(new GlobalContext());
|
||||||
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
|
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
|
||||||
|
_ec.Setup(x => x.Root).Returns(new GitHub.Runner.Worker.ExecutionContext());
|
||||||
var variables = new Dictionary<string, VariableValue>();
|
var variables = new Dictionary<string, VariableValue>();
|
||||||
if (enableComposite)
|
if (enableComposite)
|
||||||
{
|
{
|
||||||
@@ -2156,8 +2157,8 @@ runs:
|
|||||||
_dockerManager.Setup(x => x.DockerBuild(_ec.Object, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(0));
|
_dockerManager.Setup(x => x.DockerBuild(_ec.Object, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(0));
|
||||||
|
|
||||||
_jobServer = new Mock<IJobServer>();
|
_jobServer = new Mock<IJobServer>();
|
||||||
_jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>()))
|
_jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>()))
|
||||||
.Returns((Guid scopeIdentifier, string hubName, Guid planId, ActionReferenceList actions, CancellationToken cancellationToken) =>
|
.Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) =>
|
||||||
{
|
{
|
||||||
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
|
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
|
||||||
foreach (var action in actions.Actions)
|
foreach (var action in actions.Actions)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
await _actionRunner.RunAsync();
|
await _actionRunner.RunAsync();
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
_ec.Verify(x => x.SetGitHubContext("event_path", Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "_github_workflow", "event.json")), Times.Once);
|
_ec.Verify(x => x.WriteWebhookPayload(), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
289
src/Test/L0/Worker/CreateStepSummaryCommandL0.cs
Normal file
289
src/Test/L0/Worker/CreateStepSummaryCommandL0.cs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using GitHub.Runner.Common.Util;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Runner.Worker;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using DTWebApi = GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common.Tests.Worker
|
||||||
|
{
|
||||||
|
public sealed class CreateStepSummaryCommandL0
|
||||||
|
{
|
||||||
|
private Mock<IExecutionContext> _executionContext;
|
||||||
|
private Mock<IJobServerQueue> _jobServerQueue;
|
||||||
|
private ExecutionContext _jobExecutionContext;
|
||||||
|
private List<Tuple<DTWebApi.Issue, string>> _issues;
|
||||||
|
private Variables _variables;
|
||||||
|
private string _rootDirectory;
|
||||||
|
private CreateStepSummaryCommand _createStepCommand;
|
||||||
|
private ITraceWriter _trace;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void CreateStepSummaryCommand_FeatureDisabled()
|
||||||
|
{
|
||||||
|
using (var hostContext = Setup(featureFlagState: "false"))
|
||||||
|
{
|
||||||
|
var stepSummaryFile = Path.Combine(_rootDirectory, "feature-off");
|
||||||
|
|
||||||
|
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||||
|
_jobExecutionContext.Complete();
|
||||||
|
|
||||||
|
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
|
||||||
|
Assert.Equal(0, _issues.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void CreateStepSummaryCommand_FileNull()
|
||||||
|
{
|
||||||
|
using (var hostContext = Setup())
|
||||||
|
{
|
||||||
|
_createStepCommand.ProcessCommand(_executionContext.Object, null, null);
|
||||||
|
_jobExecutionContext.Complete();
|
||||||
|
|
||||||
|
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
|
||||||
|
Assert.Equal(0, _issues.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void CreateStepSummaryCommand_DirectoryNotFound()
|
||||||
|
{
|
||||||
|
using (var hostContext = Setup())
|
||||||
|
{
|
||||||
|
var stepSummaryFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
|
||||||
|
|
||||||
|
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||||
|
_jobExecutionContext.Complete();
|
||||||
|
|
||||||
|
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
|
||||||
|
Assert.Equal(0, _issues.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void CreateStepSummaryCommand_FileNotFound()
|
||||||
|
{
|
||||||
|
using (var hostContext = Setup())
|
||||||
|
{
|
||||||
|
var stepSummaryFile = Path.Combine(_rootDirectory, "file-not-found");
|
||||||
|
|
||||||
|
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||||
|
_jobExecutionContext.Complete();
|
||||||
|
|
||||||
|
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
|
||||||
|
Assert.Equal(0, _issues.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void CreateStepSummaryCommand_EmptyFile()
|
||||||
|
{
|
||||||
|
using (var hostContext = Setup())
|
||||||
|
{
|
||||||
|
var stepSummaryFile = Path.Combine(_rootDirectory, "empty-file");
|
||||||
|
File.Create(stepSummaryFile).Dispose();
|
||||||
|
|
||||||
|
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||||
|
_jobExecutionContext.Complete();
|
||||||
|
|
||||||
|
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
|
||||||
|
Assert.Equal(0, _issues.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void CreateStepSummaryCommand_LargeFile()
|
||||||
|
{
|
||||||
|
using (var hostContext = Setup())
|
||||||
|
{
|
||||||
|
var stepSummaryFile = Path.Combine(_rootDirectory, "empty-file");
|
||||||
|
File.WriteAllBytes(stepSummaryFile, new byte[128 * 1024 + 1]);
|
||||||
|
|
||||||
|
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||||
|
_jobExecutionContext.Complete();
|
||||||
|
|
||||||
|
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never());
|
||||||
|
Assert.Equal(1, _issues.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void CreateStepSummaryCommand_Simple()
|
||||||
|
{
|
||||||
|
using (var hostContext = Setup())
|
||||||
|
{
|
||||||
|
var stepSummaryFile = Path.Combine(_rootDirectory, "simple");
|
||||||
|
var content = new List<string>
|
||||||
|
{
|
||||||
|
"# This is some markdown content",
|
||||||
|
"",
|
||||||
|
"## This is more markdown content",
|
||||||
|
};
|
||||||
|
WriteContent(stepSummaryFile, content);
|
||||||
|
|
||||||
|
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||||
|
_jobExecutionContext.Complete();
|
||||||
|
|
||||||
|
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), ChecksAttachmentType.StepSummary, _executionContext.Object.Id.ToString(), stepSummaryFile + "-scrubbed", It.IsAny<bool>()), Times.Once());
|
||||||
|
Assert.Equal(0, _issues.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void CreateStepSummaryCommand_ScrubSecrets()
|
||||||
|
{
|
||||||
|
using (var hostContext = Setup())
|
||||||
|
{
|
||||||
|
// configure secretmasker to actually mask secrets
|
||||||
|
hostContext.SecretMasker.AddRegex("Password=.*");
|
||||||
|
hostContext.SecretMasker.AddRegex("ghs_.*");
|
||||||
|
|
||||||
|
var stepSummaryFile = Path.Combine(_rootDirectory, "simple");
|
||||||
|
var scrubbedFile = stepSummaryFile + "-scrubbed";
|
||||||
|
var content = new List<string>
|
||||||
|
{
|
||||||
|
"# Password=ThisIsMySecretPassword!",
|
||||||
|
"",
|
||||||
|
"# GITHUB_TOKEN ghs_verysecuretoken",
|
||||||
|
};
|
||||||
|
WriteContent(stepSummaryFile, content);
|
||||||
|
|
||||||
|
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||||
|
|
||||||
|
var scrubbedFileContents = File.ReadAllText(scrubbedFile);
|
||||||
|
Assert.DoesNotContain("ThisIsMySecretPassword!", scrubbedFileContents);
|
||||||
|
Assert.DoesNotContain("ghs_verysecuretoken", scrubbedFileContents);
|
||||||
|
_jobExecutionContext.Complete();
|
||||||
|
|
||||||
|
_jobServerQueue.Verify(x => x.QueueFileUpload(It.IsAny<Guid>(), It.IsAny<Guid>(), ChecksAttachmentType.StepSummary, _executionContext.Object.Id.ToString(), scrubbedFile, It.IsAny<bool>()), Times.Once());
|
||||||
|
Assert.Equal(0, _issues.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteContent(
|
||||||
|
string path,
|
||||||
|
List<string> content,
|
||||||
|
string newline = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(newline))
|
||||||
|
{
|
||||||
|
newline = Environment.NewLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoding = new UTF8Encoding(true); // Emit BOM
|
||||||
|
var contentStr = string.Join(newline, content);
|
||||||
|
File.WriteAllText(path, contentStr, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestHostContext Setup([CallerMemberName] string name = "", string featureFlagState = "true")
|
||||||
|
{
|
||||||
|
var hostContext = new TestHostContext(this, name);
|
||||||
|
|
||||||
|
_issues = new List<Tuple<DTWebApi.Issue, string>>();
|
||||||
|
|
||||||
|
// Setup a job request
|
||||||
|
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||||
|
TimelineReference timeline = new TimelineReference();
|
||||||
|
Guid jobId = Guid.NewGuid();
|
||||||
|
string jobName = "Summary Job";
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
|
||||||
|
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
|
||||||
|
{
|
||||||
|
Alias = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Id = "github",
|
||||||
|
Version = "sha1"
|
||||||
|
});
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
jobRequest.Variables["ACTIONS_STEP_DEBUG"] = "true";
|
||||||
|
|
||||||
|
// Server queue for job
|
||||||
|
_jobServerQueue = new Mock<IJobServerQueue>();
|
||||||
|
_jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||||
|
hostContext.SetSingleton(_jobServerQueue.Object);
|
||||||
|
|
||||||
|
// Configuration store (required singleton)
|
||||||
|
var configurationStore = new Mock<IConfigurationStore>();
|
||||||
|
configurationStore.Setup(x => x.GetSettings()).Returns(new RunnerSettings());
|
||||||
|
hostContext.SetSingleton(configurationStore.Object);
|
||||||
|
|
||||||
|
// Paging Logger (required singleton)
|
||||||
|
var pagingLogger = new Mock<IPagingLogger>();
|
||||||
|
hostContext.EnqueueInstance(pagingLogger.Object);
|
||||||
|
|
||||||
|
// Trace
|
||||||
|
_trace = hostContext.GetTrace();
|
||||||
|
|
||||||
|
// Variables to test for secret scrubbing & FF options
|
||||||
|
_variables = new Variables(hostContext, new Dictionary<string, VariableValue>
|
||||||
|
{
|
||||||
|
{ "MySecretName", new VariableValue("My secret value", true) },
|
||||||
|
{ "DistributedTask.UploadStepSummary", featureFlagState },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Directory for test data
|
||||||
|
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
|
||||||
|
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
|
||||||
|
Directory.CreateDirectory(workDirectory);
|
||||||
|
_rootDirectory = Path.Combine(workDirectory, nameof(CreateStepSummaryCommandL0));
|
||||||
|
Directory.CreateDirectory(_rootDirectory);
|
||||||
|
|
||||||
|
// Job execution context
|
||||||
|
_jobExecutionContext = new ExecutionContext();
|
||||||
|
_jobExecutionContext.Initialize(hostContext);
|
||||||
|
_jobExecutionContext.InitializeJob(jobRequest, System.Threading.CancellationToken.None);
|
||||||
|
|
||||||
|
// Step execution context
|
||||||
|
_executionContext = new Mock<IExecutionContext>();
|
||||||
|
_executionContext.Setup(x => x.Global)
|
||||||
|
.Returns(new GlobalContext
|
||||||
|
{
|
||||||
|
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
|
||||||
|
WriteDebug = true,
|
||||||
|
Variables = _variables,
|
||||||
|
});
|
||||||
|
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
|
||||||
|
.Callback((DTWebApi.Issue issue, string logMessage) =>
|
||||||
|
{
|
||||||
|
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
|
||||||
|
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
|
||||||
|
_trace.Info($"Issue '{issue.Type}': {message}");
|
||||||
|
});
|
||||||
|
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
|
||||||
|
.Callback((string tag, string message) =>
|
||||||
|
{
|
||||||
|
_trace.Info($"{tag}{message}");
|
||||||
|
});
|
||||||
|
_executionContext.SetupGet(x => x.Root).Returns(_jobExecutionContext);
|
||||||
|
|
||||||
|
//CreateStepSummaryCommand
|
||||||
|
_createStepCommand = new CreateStepSummaryCommand();
|
||||||
|
_createStepCommand.Initialize(hostContext);
|
||||||
|
|
||||||
|
return hostContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
using System;
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Worker;
|
|
||||||
using Moq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Worker;
|
||||||
|
using GitHub.Runner.Worker.Container;
|
||||||
|
using GitHub.Runner.Worker.Handlers;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests.Worker
|
namespace GitHub.Runner.Common.Tests.Worker
|
||||||
@@ -90,6 +93,166 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void ApplyContinueOnError_CheckResultAndOutcome()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
|
||||||
|
// Arrange: Create a job request message.
|
||||||
|
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||||
|
TimelineReference timeline = new TimelineReference();
|
||||||
|
Guid jobId = Guid.NewGuid();
|
||||||
|
string jobName = "some job name";
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
|
||||||
|
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
|
||||||
|
{
|
||||||
|
Alias = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Id = "github",
|
||||||
|
Version = "sha1"
|
||||||
|
});
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
jobRequest.Variables["ACTIONS_STEP_DEBUG"] = "true";
|
||||||
|
|
||||||
|
// Arrange: Setup the paging logger.
|
||||||
|
var pagingLogger = new Mock<IPagingLogger>();
|
||||||
|
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||||
|
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||||
|
jobServerQueue.Setup(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<long>())).Callback((Guid id, string msg, long? lineNumber) => { hc.GetTrace().Info(msg); });
|
||||||
|
|
||||||
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
|
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
|
||||||
|
foreach (var tc in new List<(TemplateToken token, TaskResult result, TaskResult? expectedResult, TaskResult? expectedOutcome)> {
|
||||||
|
(token: new BooleanToken(null, null, null, true), result: TaskResult.Failed, expectedResult: TaskResult.Succeeded, expectedOutcome: TaskResult.Failed),
|
||||||
|
(token: new BooleanToken(null, null, null, true), result: TaskResult.Succeeded, expectedResult: TaskResult.Succeeded, expectedOutcome: null),
|
||||||
|
(token: new BooleanToken(null, null, null, true), result: TaskResult.Canceled, expectedResult: TaskResult.Canceled, expectedOutcome: null),
|
||||||
|
(token: new BooleanToken(null, null, null, false), result: TaskResult.Failed, expectedResult: TaskResult.Failed, expectedOutcome: null),
|
||||||
|
(token: new BooleanToken(null, null, null, false), result: TaskResult.Succeeded, expectedResult: TaskResult.Succeeded, expectedOutcome: null),
|
||||||
|
(token: new BooleanToken(null, null, null, false), result: TaskResult.Canceled, expectedResult: TaskResult.Canceled, expectedOutcome: null),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
ec.Result = tc.result;
|
||||||
|
ec.Outcome = null;
|
||||||
|
ec.ApplyContinueOnError(tc.token);
|
||||||
|
Assert.Equal(ec.Result, tc.expectedResult);
|
||||||
|
Assert.Equal(ec.Outcome, tc.expectedOutcome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void AddIssue_TrimMessageSize()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
// Arrange: Create a job request message.
|
||||||
|
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||||
|
TimelineReference timeline = new TimelineReference();
|
||||||
|
Guid jobId = Guid.NewGuid();
|
||||||
|
string jobName = "some job name";
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
|
||||||
|
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
|
||||||
|
{
|
||||||
|
Alias = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Id = "github",
|
||||||
|
Version = "sha1"
|
||||||
|
});
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
|
||||||
|
// Arrange: Setup the paging logger.
|
||||||
|
var pagingLogger = new Mock<IPagingLogger>();
|
||||||
|
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||||
|
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||||
|
|
||||||
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
|
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
|
||||||
|
var bigMessage = "";
|
||||||
|
for (var i = 0; i < 5000; i++)
|
||||||
|
{
|
||||||
|
bigMessage += "a";
|
||||||
|
}
|
||||||
|
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = bigMessage });
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = bigMessage });
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = bigMessage });
|
||||||
|
|
||||||
|
ec.Complete();
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Type == IssueType.Error).Single().Message.Length <= 4096)), Times.AtLeastOnce);
|
||||||
|
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Type == IssueType.Warning).Single().Message.Length <= 4096)), Times.AtLeastOnce);
|
||||||
|
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Type == IssueType.Notice).Single().Message.Length <= 4096)), Times.AtLeastOnce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void AddIssue_AddStepAndLineNumberInformation()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
|
||||||
|
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||||
|
TimelineReference timeline = new TimelineReference();
|
||||||
|
Guid jobId = Guid.NewGuid();
|
||||||
|
string jobName = "some job name";
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
|
||||||
|
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
|
||||||
|
{
|
||||||
|
Alias = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Id = "github",
|
||||||
|
Version = "sha1"
|
||||||
|
});
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
|
||||||
|
// Arrange: Setup the paging logger.
|
||||||
|
var pagingLogger = new Mock<IPagingLogger>();
|
||||||
|
var pagingLogger2 = new Mock<IPagingLogger>();
|
||||||
|
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||||
|
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||||
|
|
||||||
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
|
hc.EnqueueInstance(pagingLogger2.Object);
|
||||||
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
|
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
ec.Start();
|
||||||
|
|
||||||
|
var embeddedStep = ec.CreateChild(Guid.NewGuid(), "action_1_pre", "action_1_pre", null, null, ActionRunStage.Main, isEmbedded: true);
|
||||||
|
embeddedStep.Start();
|
||||||
|
|
||||||
|
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error annotation that should have step and line number information" });
|
||||||
|
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning annotation that should have step and line number information" });
|
||||||
|
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice annotation that should have step and line number information" });
|
||||||
|
|
||||||
|
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Error).Count() == 1)), Times.AtLeastOnce);
|
||||||
|
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Warning).Count() == 1)), Times.AtLeastOnce);
|
||||||
|
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Notice).Count() == 1)), Times.AtLeastOnce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
@@ -116,7 +279,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
var pagingLogger = new Mock<IPagingLogger>();
|
var pagingLogger = new Mock<IPagingLogger>();
|
||||||
var jobServerQueue = new Mock<IJobServerQueue>();
|
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||||
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||||
jobServerQueue.Setup(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.IsAny<string>(),It.IsAny<long>())).Callback((Guid id, string msg, long? lineNumber) => { hc.GetTrace().Info(msg); });
|
jobServerQueue.Setup(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<long>())).Callback((Guid id, string msg, long? lineNumber) => { hc.GetTrace().Info(msg); });
|
||||||
|
|
||||||
hc.EnqueueInstance(pagingLogger.Object);
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
hc.SetSingleton(jobServerQueue.Object);
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
@@ -378,6 +541,177 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void PublishStepTelemetry_RegularStep_NoOpt()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
// Arrange: Create a job request message.
|
||||||
|
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||||
|
TimelineReference timeline = new TimelineReference();
|
||||||
|
Guid jobId = Guid.NewGuid();
|
||||||
|
string jobName = "some job name";
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
|
||||||
|
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
|
||||||
|
{
|
||||||
|
Alias = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Id = "github",
|
||||||
|
Version = "sha1"
|
||||||
|
});
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
|
||||||
|
// Arrange: Setup the paging logger.
|
||||||
|
var pagingLogger = new Mock<IPagingLogger>();
|
||||||
|
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||||
|
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||||
|
|
||||||
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
|
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
ec.Start();
|
||||||
|
|
||||||
|
ec.Complete();
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(0, ec.Global.StepsTelemetry.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void PublishStepTelemetry_RegularStep()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
// Arrange: Create a job request message.
|
||||||
|
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||||
|
TimelineReference timeline = new TimelineReference();
|
||||||
|
Guid jobId = Guid.NewGuid();
|
||||||
|
string jobName = "some job name";
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
|
||||||
|
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
|
||||||
|
{
|
||||||
|
Alias = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Id = "github",
|
||||||
|
Version = "sha1"
|
||||||
|
});
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
|
||||||
|
// Arrange: Setup the paging logger.
|
||||||
|
var pagingLogger = new Mock<IPagingLogger>();
|
||||||
|
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||||
|
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||||
|
|
||||||
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
|
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
ec.Start();
|
||||||
|
|
||||||
|
ec.StepTelemetry.Type = "node16";
|
||||||
|
ec.StepTelemetry.Action = "actions/checkout";
|
||||||
|
ec.StepTelemetry.Ref = "v2";
|
||||||
|
ec.StepTelemetry.IsEmbedded = false;
|
||||||
|
ec.StepTelemetry.StepId = Guid.NewGuid();
|
||||||
|
ec.StepTelemetry.Stage = "main";
|
||||||
|
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
|
||||||
|
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
|
||||||
|
|
||||||
|
ec.Complete();
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(1, ec.Global.StepsTelemetry.Count);
|
||||||
|
Assert.Equal("node16", ec.Global.StepsTelemetry.Single().Type);
|
||||||
|
Assert.Equal("actions/checkout", ec.Global.StepsTelemetry.Single().Action);
|
||||||
|
Assert.Equal("v2", ec.Global.StepsTelemetry.Single().Ref);
|
||||||
|
Assert.Equal(TaskResult.Succeeded, ec.Global.StepsTelemetry.Single().Result);
|
||||||
|
Assert.NotNull(ec.Global.StepsTelemetry.Single().ExecutionTimeInSeconds);
|
||||||
|
Assert.Equal(3, ec.Global.StepsTelemetry.Single().ErrorMessages.Count);
|
||||||
|
Assert.DoesNotContain(ec.Global.StepsTelemetry.Single().ErrorMessages, x => x.Contains("notice"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void PublishStepTelemetry_EmbeddedStep()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
// Arrange: Create a job request message.
|
||||||
|
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||||
|
TimelineReference timeline = new TimelineReference();
|
||||||
|
Guid jobId = Guid.NewGuid();
|
||||||
|
string jobName = "some job name";
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
|
||||||
|
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
|
||||||
|
{
|
||||||
|
Alias = Pipelines.PipelineConstants.SelfAlias,
|
||||||
|
Id = "github",
|
||||||
|
Version = "sha1"
|
||||||
|
});
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
|
||||||
|
// Arrange: Setup the paging logger.
|
||||||
|
var pagingLogger = new Mock<IPagingLogger>();
|
||||||
|
var pagingLogger2 = new Mock<IPagingLogger>();
|
||||||
|
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||||
|
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||||
|
|
||||||
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
|
hc.EnqueueInstance(pagingLogger2.Object);
|
||||||
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
|
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
ec.Start();
|
||||||
|
|
||||||
|
var embeddedStep = ec.CreateChild(Guid.NewGuid(), "action_1_pre", "action_1_pre", null, null, ActionRunStage.Main, isEmbedded: true);
|
||||||
|
embeddedStep.Start();
|
||||||
|
|
||||||
|
embeddedStep.StepTelemetry.Type = "node16";
|
||||||
|
embeddedStep.StepTelemetry.Action = "actions/checkout";
|
||||||
|
embeddedStep.StepTelemetry.Ref = "v2";
|
||||||
|
|
||||||
|
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
|
||||||
|
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
|
||||||
|
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
|
||||||
|
|
||||||
|
embeddedStep.PublishStepTelemetry();
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(1, ec.Global.StepsTelemetry.Count);
|
||||||
|
Assert.Equal("node16", ec.Global.StepsTelemetry.Single().Type);
|
||||||
|
Assert.Equal("actions/checkout", ec.Global.StepsTelemetry.Single().Action);
|
||||||
|
Assert.Equal("v2", ec.Global.StepsTelemetry.Single().Ref);
|
||||||
|
Assert.Equal(ActionRunStage.Main.ToString(), ec.Global.StepsTelemetry.Single().Stage);
|
||||||
|
Assert.True(ec.Global.StepsTelemetry.Single().IsEmbedded);
|
||||||
|
Assert.Null(ec.Global.StepsTelemetry.Single().Result);
|
||||||
|
Assert.Null(ec.Global.StepsTelemetry.Single().ExecutionTimeInSeconds);
|
||||||
|
Assert.Equal(0, ec.Global.StepsTelemetry.Single().ErrorMessages.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
|
private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
|
||||||
{
|
{
|
||||||
var hc = new TestHostContext(this, testName);
|
var hc = new TestHostContext(this, testName);
|
||||||
@@ -392,5 +726,149 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
|
|
||||||
return hc;
|
return hc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void GetExpressionValues_ContainerStepHost()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
const string source = "/home/username/Projects/work/runner/_layout";
|
||||||
|
var containerInfo = new ContainerInfo();
|
||||||
|
containerInfo.ContainerId = "test";
|
||||||
|
|
||||||
|
containerInfo.AddPathTranslateMapping($"{source}/_work", "/__w");
|
||||||
|
containerInfo.AddPathTranslateMapping($"{source}/_temp", "/__t");
|
||||||
|
containerInfo.AddPathTranslateMapping($"{source}/externals", "/__e");
|
||||||
|
|
||||||
|
containerInfo.AddPathTranslateMapping($"{source}/_work/_temp/_github_home", "/github/home");
|
||||||
|
containerInfo.AddPathTranslateMapping($"{source}/_work/_temp/_github_workflow", "/github/workflow");
|
||||||
|
|
||||||
|
foreach (var v in new List<string>() {
|
||||||
|
$"{source}/_work",
|
||||||
|
$"{source}/externals",
|
||||||
|
$"{source}/_work/_temp",
|
||||||
|
$"{source}/_work/_actions",
|
||||||
|
$"{source}/_work/_tool",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
containerInfo.MountVolumes.Add(new MountVolume(v, containerInfo.TranslateToContainerPath(v)));
|
||||||
|
};
|
||||||
|
|
||||||
|
var stepHost = new ContainerStepHost();
|
||||||
|
stepHost.Container = containerInfo;
|
||||||
|
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
|
||||||
|
var inputGithubContext = new GitHubContext();
|
||||||
|
var inputeRunnerContext = new RunnerContext();
|
||||||
|
|
||||||
|
// string context data
|
||||||
|
inputGithubContext["action_path"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_actions/owner/composite/main");
|
||||||
|
inputGithubContext["action"] = new StringContextData("__owner_composite");
|
||||||
|
inputGithubContext["api_url"] = new StringContextData("https://api.github.com/custom/path");
|
||||||
|
inputGithubContext["env"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_temp/_runner_file_commands/set_env_265698aa-7f38-40f5-9316-5c01a3153672");
|
||||||
|
inputGithubContext["path"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_temp/_runner_file_commands/add_path_265698aa-7f38-40f5-9316-5c01a3153672");
|
||||||
|
inputGithubContext["event_path"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_temp/_github_workflow/event.json");
|
||||||
|
inputGithubContext["repository"] = new StringContextData("owner/repo-name");
|
||||||
|
inputGithubContext["run_id"] = new StringContextData("2033211332");
|
||||||
|
inputGithubContext["workflow"] = new StringContextData("Name of Workflow");
|
||||||
|
inputGithubContext["workspace"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/step-order/step-order");
|
||||||
|
inputeRunnerContext["temp"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_temp");
|
||||||
|
inputeRunnerContext["tool_cache"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_tool");
|
||||||
|
|
||||||
|
// dictionary context data
|
||||||
|
var githubEvent = new DictionaryContextData();
|
||||||
|
githubEvent["inputs"] = null;
|
||||||
|
githubEvent["ref"] = new StringContextData("refs/heads/main");
|
||||||
|
githubEvent["repository"] = new DictionaryContextData();
|
||||||
|
githubEvent["sender"] = new DictionaryContextData();
|
||||||
|
githubEvent["workflow"] = new StringContextData(".github/workflows/composite_step_host_translate.yaml");
|
||||||
|
|
||||||
|
inputGithubContext["event"] = githubEvent;
|
||||||
|
|
||||||
|
ec.ExpressionValues["github"] = inputGithubContext;
|
||||||
|
ec.ExpressionValues["runner"] = inputeRunnerContext;
|
||||||
|
|
||||||
|
var ecExpect = new Runner.Worker.ExecutionContext();
|
||||||
|
ecExpect.Initialize(hc);
|
||||||
|
|
||||||
|
var expectedGithubEvent = new DictionaryContextData();
|
||||||
|
expectedGithubEvent["inputs"] = null;
|
||||||
|
expectedGithubEvent["ref"] = new StringContextData("refs/heads/main");
|
||||||
|
expectedGithubEvent["repository"] = new DictionaryContextData();
|
||||||
|
expectedGithubEvent["sender"] = new DictionaryContextData();
|
||||||
|
expectedGithubEvent["workflow"] = new StringContextData(".github/workflows/composite_step_host_translate.yaml");
|
||||||
|
var expectedGithubContext = new GitHubContext();
|
||||||
|
var expectedRunnerContext = new RunnerContext();
|
||||||
|
expectedGithubContext["action_path"] = new StringContextData("/__w/_actions/owner/composite/main");
|
||||||
|
expectedGithubContext["action"] = new StringContextData("__owner_composite");
|
||||||
|
expectedGithubContext["api_url"] = new StringContextData("https://api.github.com/custom/path");
|
||||||
|
expectedGithubContext["env"] = new StringContextData("/__w/_temp/_runner_file_commands/set_env_265698aa-7f38-40f5-9316-5c01a3153672");
|
||||||
|
expectedGithubContext["path"] = new StringContextData("/__w/_temp/_runner_file_commands/add_path_265698aa-7f38-40f5-9316-5c01a3153672");
|
||||||
|
expectedGithubContext["event_path"] = new StringContextData("/github/workflow/event.json");
|
||||||
|
expectedGithubContext["repository"] = new StringContextData("owner/repo-name");
|
||||||
|
expectedGithubContext["run_id"] = new StringContextData("2033211332");
|
||||||
|
expectedGithubContext["workflow"] = new StringContextData("Name of Workflow");
|
||||||
|
expectedGithubContext["workspace"] = new StringContextData("/__w/step-order/step-order");
|
||||||
|
expectedGithubContext["event"] = expectedGithubEvent;
|
||||||
|
expectedRunnerContext["temp"] = new StringContextData("/__w/_temp");
|
||||||
|
expectedRunnerContext["tool_cache"] = new StringContextData("/__w/_tool");
|
||||||
|
|
||||||
|
ecExpect.ExpressionValues["github"] = expectedGithubContext;
|
||||||
|
ecExpect.ExpressionValues["runner"] = expectedRunnerContext;
|
||||||
|
|
||||||
|
var translatedExpressionValues = ec.GetExpressionValues(stepHost);
|
||||||
|
|
||||||
|
foreach (var contextName in new string[] { "github", "runner" })
|
||||||
|
{
|
||||||
|
var dict = translatedExpressionValues[contextName].AssertDictionary($"expected context github to be a dictionary");
|
||||||
|
var expectedExpressionValues = ecExpect.ExpressionValues[contextName].AssertDictionary("expect dict");
|
||||||
|
foreach (var key in dict.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (dict[key] is StringContextData)
|
||||||
|
{
|
||||||
|
var expect = dict[key].AssertString("expect string");
|
||||||
|
var outcome = expectedExpressionValues[key].AssertString("expect string");
|
||||||
|
Assert.Equal(expect.Value, outcome.Value);
|
||||||
|
}
|
||||||
|
else if (dict[key] is DictionaryContextData || dict[key] is CaseSensitiveDictionaryContextData)
|
||||||
|
{
|
||||||
|
var expectDict = dict[key].AssertDictionary("expect dict");
|
||||||
|
var actualDict = expectedExpressionValues[key].AssertDictionary("expect dict");
|
||||||
|
Assert.True(ExpressionValuesAssertEqual(expectDict, actualDict));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ExpressionValuesAssertEqual(DictionaryContextData expect, DictionaryContextData actual)
|
||||||
|
{
|
||||||
|
foreach (var key in expect.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (expect[key] is StringContextData)
|
||||||
|
{
|
||||||
|
var expectValue = expect[key].AssertString("expect string");
|
||||||
|
var actualValue = actual[key].AssertString("expect string");
|
||||||
|
if (expectValue.Equals(actualValue))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (expect[key] is DictionaryContextData || expect[key] is CaseSensitiveDictionaryContextData)
|
||||||
|
{
|
||||||
|
var expectDict = expect[key].AssertDictionary("expect dict");
|
||||||
|
var actualDict = actual[key].AssertDictionary("expect dict");
|
||||||
|
if (!ExpressionValuesAssertEqual(expectDict, actualDict))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user