mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:36:23 +00:00
Compare commits
300 Commits
v2.168.0
...
users/tihu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5057893a13 | ||
|
|
5b8ff174c6 | ||
|
|
e3e977fd84 | ||
|
|
4dc8a09db3 | ||
|
|
dcc5d34ad1 | ||
|
|
3e34fb10c1 | ||
|
|
23a693aa2c | ||
|
|
eb36db8ff9 | ||
|
|
85e1927754 | ||
|
|
b6dbf42746 | ||
|
|
67ba8a7d42 | ||
|
|
e4f9e6ae26 | ||
|
|
854d5e3bf3 | ||
|
|
57dec28f68 | ||
|
|
55a861f089 | ||
|
|
51b2031cbf | ||
|
|
400b2d879c | ||
|
|
c4b6d288d4 | ||
|
|
0699597876 | ||
|
|
a592b14ae3 | ||
|
|
04269f7b1b | ||
|
|
e89d2e84bd | ||
|
|
afe7066e39 | ||
|
|
da79ef4acb | ||
|
|
5afb52b272 | ||
|
|
cf87c55557 | ||
|
|
43fa351980 | ||
|
|
ecfc2cc9e9 | ||
|
|
740fb43731 | ||
|
|
f259e5706f | ||
|
|
5d84918ed5 | ||
|
|
881c521005 | ||
|
|
176e7f5208 | ||
|
|
b6d46c148a | ||
|
|
38e33bb8e3 | ||
|
|
404b3418b7 | ||
|
|
7ffd9af644 | ||
|
|
1b69c279f5 | ||
|
|
567870dbb8 | ||
|
|
72fa2a8a0d | ||
|
|
4359dd605b | ||
|
|
aab936d081 | ||
|
|
777ce5a0dc | ||
|
|
1a62162708 | ||
|
|
9a829995e0 | ||
|
|
c5ce52641c | ||
|
|
e82725b580 | ||
|
|
0464f77de3 | ||
|
|
1fc159e0df | ||
|
|
3615fb6923 | ||
|
|
f61dcad5bb | ||
|
|
62d568674c | ||
|
|
07c00f6a8a | ||
|
|
05b84297b7 | ||
|
|
04679b56a9 | ||
|
|
d2ca24fa43 | ||
|
|
abdaacfa6e | ||
|
|
53fd7161e2 | ||
|
|
ce68f3b167 | ||
|
|
e2c7329292 | ||
|
|
22a9d89772 | ||
|
|
3851acd0cf | ||
|
|
aab4aca8f7 | ||
|
|
5af7b87074 | ||
|
|
110eb3a5de | ||
|
|
bd1341e580 | ||
|
|
85ce33b1d3 | ||
|
|
92ec3d0f29 | ||
|
|
4e95d0d6ad | ||
|
|
5281434f3f | ||
|
|
e9a8bf29df | ||
|
|
a65331e887 | ||
|
|
908a082527 | ||
|
|
10ba74f59b | ||
|
|
33ee76df29 | ||
|
|
592ce1b230 | ||
|
|
fff31e11c5 | ||
|
|
6443fe8c97 | ||
|
|
29c09c5bf8 | ||
|
|
09821e2169 | ||
|
|
7c90b2a929 | ||
|
|
ee34f4842e | ||
|
|
713344016d | ||
|
|
0a6c34669c | ||
|
|
40d6eb3da3 | ||
|
|
34a985f3b9 | ||
|
|
42fe704132 | ||
|
|
a1bcd5996b | ||
|
|
31584f4451 | ||
|
|
d4cdb633db | ||
|
|
11939832df | ||
|
|
ebadce7958 | ||
|
|
4d5d5b74ee | ||
|
|
ff12fae2c9 | ||
|
|
8e907b19dc | ||
|
|
93ec16e14f | ||
|
|
8863b1fb2c | ||
|
|
484ea74ed0 | ||
|
|
f21e280b5c | ||
|
|
e0643c694c | ||
|
|
508d188fb6 | ||
|
|
e7d74da160 | ||
|
|
d1f7258356 | ||
|
|
3a5ab37153 | ||
|
|
419ed24c1e | ||
|
|
7cc689b0d9 | ||
|
|
5941cceb7c | ||
|
|
088caf5337 | ||
|
|
08852bd2fc | ||
|
|
57d694197f | ||
|
|
fc4027b3f1 | ||
|
|
d14881b970 | ||
|
|
be9632302c | ||
|
|
2b5ddd7c21 | ||
|
|
8109c962f0 | ||
|
|
af198237ca | ||
|
|
1559ff15ec | ||
|
|
67ff8d3460 | ||
|
|
6cbfbc3186 | ||
|
|
195c2db5ef | ||
|
|
50994bbb3b | ||
|
|
7b03699fbe | ||
|
|
8a4cb76508 | ||
|
|
bc3099793f | ||
|
|
b76d229da0 | ||
|
|
fe3994bf1d | ||
|
|
0ae09e6713 | ||
|
|
2b4d5542aa | ||
|
|
6b0f0c00b1 | ||
|
|
09760c0d69 | ||
|
|
8f14466cbb | ||
|
|
fe8a56f81a | ||
|
|
59b30262ac | ||
|
|
9efcec38cc | ||
|
|
5972bd0060 | ||
|
|
239cc0d7ca | ||
|
|
3fb915450a | ||
|
|
4b6ded0a01 | ||
|
|
0953ffa62b | ||
|
|
66727f76c8 | ||
|
|
7ee333b5cd | ||
|
|
3b34e203dc | ||
|
|
e808190dd2 | ||
|
|
d2cb9d7685 | ||
|
|
5ba6a2c78d | ||
|
|
fc3ca9bb92 | ||
|
|
a94a19bb36 | ||
|
|
a9be5f6557 | ||
|
|
3600f20cd3 | ||
|
|
81a00fff3e | ||
|
|
31474098ff | ||
|
|
7ff6ff6afa | ||
|
|
56529a1c2f | ||
|
|
510fadf71a | ||
|
|
007ac8138b | ||
|
|
1e12b8909a | ||
|
|
9ceb3d481a | ||
|
|
3bce2eb09c | ||
|
|
80bf68db81 | ||
|
|
a2e32170fd | ||
|
|
35dda19491 | ||
|
|
36bdf50bc6 | ||
|
|
95e2158dc6 | ||
|
|
3ebaeb9f19 | ||
|
|
9d678cb270 | ||
|
|
27788491ea | ||
|
|
5ba7affea4 | ||
|
|
ce92d7a6b5 | ||
|
|
d23ca0ba7a | ||
|
|
9d1c81f018 | ||
|
|
7a8abe726a | ||
|
|
a9135e61a0 | ||
|
|
feafd3e1d7 | ||
|
|
dc3b2d3a36 | ||
|
|
a371309079 | ||
|
|
5dd6bde4ca | ||
|
|
c196103e58 | ||
|
|
d55070da3e | ||
|
|
8279ae9a70 | ||
|
|
2e3b03623f | ||
|
|
c18c8746db | ||
|
|
6332a52d76 | ||
|
|
8bb588bb69 | ||
|
|
4510f69c73 | ||
|
|
c7b8552edf | ||
|
|
0face6e3af | ||
|
|
306be41266 | ||
|
|
4e85b8f3b7 | ||
|
|
444332ca88 | ||
|
|
e6eb9e381d | ||
|
|
3a76a2e291 | ||
|
|
9976cb92a0 | ||
|
|
d900654c42 | ||
|
|
65e3ec86b4 | ||
|
|
a7f205593a | ||
|
|
55f60a4ffc | ||
|
|
ca13b25240 | ||
|
|
b0c2734380 | ||
|
|
9e7b56f698 | ||
|
|
8c29e33e88 | ||
|
|
976217d6ec | ||
|
|
562eafab3a | ||
|
|
9015b95a72 | ||
|
|
7d4bbf46de | ||
|
|
7b608e3e92 | ||
|
|
f028b4e2b0 | ||
|
|
38f816c2ae | ||
|
|
bc1fe2cfe0 | ||
|
|
89a13db2c3 | ||
|
|
d59092d973 | ||
|
|
855b90c3d4 | ||
|
|
48ac96307c | ||
|
|
2e50dffb37 | ||
|
|
e7b0844772 | ||
|
|
d5a5550649 | ||
|
|
3d0147d322 | ||
|
|
bd1f245aac | ||
|
|
005f1c15b1 | ||
|
|
da3cb5506f | ||
|
|
32d439070b | ||
|
|
ec9f8f1682 | ||
|
|
0921af735a | ||
|
|
1cc3c08cf2 | ||
|
|
f9dca15c63 | ||
|
|
0877d9a533 | ||
|
|
d5e40c6a60 | ||
|
|
391bc35bb9 | ||
|
|
e4267b8434 | ||
|
|
2709cbc0ea | ||
|
|
5e0cde8649 | ||
|
|
cb2b323781 | ||
|
|
6c3958f365 | ||
|
|
9d7bd4706b | ||
|
|
5822a38c39 | ||
|
|
d42c9da2d7 | ||
|
|
121deedeb5 | ||
|
|
a0942ed345 | ||
|
|
7cef9a27ca | ||
|
|
df7e16954e | ||
|
|
4e7d27a53c | ||
|
|
89d1418e48 | ||
|
|
e728b8594d | ||
|
|
de4490d06d | ||
|
|
2e800f857e | ||
|
|
312c7668a8 | ||
|
|
eaf39bb058 | ||
|
|
5815819f24 | ||
|
|
1aea046932 | ||
|
|
eda463601c | ||
|
|
f994ae0542 | ||
|
|
3c5aef791c | ||
|
|
c4626d0c3a | ||
|
|
416a7ac4b8 | ||
|
|
11435857e4 | ||
|
|
6f260012a3 | ||
|
|
4fc87ddfc6 | ||
|
|
b45c1b9440 | ||
|
|
73307c0a30 | ||
|
|
cd8e4ddba1 | ||
|
|
abf59bdcb6 | ||
|
|
09cf59c1e0 | ||
|
|
7a65236022 | ||
|
|
462b5117c8 | ||
|
|
6922f3cb86 | ||
|
|
911135e66c | ||
|
|
01c9a8a8af | ||
|
|
33d2d2c328 | ||
|
|
a246b3b29d | ||
|
|
c7768d4a7b | ||
|
|
70729fb3c4 | ||
|
|
1470a3b6e2 | ||
|
|
2fadf430e4 | ||
|
|
f798f5606b | ||
|
|
3f7a01af93 | ||
|
|
d5c54f9819 | ||
|
|
9f78ad3b34 | ||
|
|
97883c8cd5 | ||
|
|
c5fa9fb062 | ||
|
|
b2dcdc21dc | ||
|
|
c126b52fe5 | ||
|
|
117ec1fff9 | ||
|
|
d5c7097d2c | ||
|
|
f9baec4b32 | ||
|
|
a20ad4e121 | ||
|
|
2bd0b1af0e | ||
|
|
baa6ded3bc | ||
|
|
7817e1a976 | ||
|
|
d90273a068 | ||
|
|
2cdde6cb16 | ||
|
|
1f52dfa636 | ||
|
|
83b5742278 | ||
|
|
ba69b5bc93 | ||
|
|
0e8777ebda | ||
|
|
a5f06b3ec2 | ||
|
|
be325f26a6 | ||
|
|
dec260920f | ||
|
|
b0a1294ef5 | ||
|
|
3d70ef2da1 | ||
|
|
e23d68f6e2 | ||
|
|
dff1024cd3 |
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
# https://editorconfig.org/
|
||||
|
||||
[*]
|
||||
insert_final_newline = true # ensure all files end with a single newline
|
||||
trim_trailing_whitespace = true # attempt to remove trailing whitespace on save
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false # in markdown, "two trailing spaces" is unfortunately meaningful; it means `<br>`
|
||||
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -20,7 +20,7 @@
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
@@ -70,9 +70,9 @@
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
*.doc diff=astextplain
|
||||
|
||||
@@ -24,4 +24,4 @@ If applicable, add a code snippet.
|
||||
**Additional information**
|
||||
Add any other context about the feature here.
|
||||
|
||||
NOTE: if the feature request has been agreed upon then the assignee will create an ADR. See docs/adrs/README.md
|
||||
NOTE: if the feature request has been agreed upon then the assignee will create an ADR. See docs/adrs/README.md
|
||||
|
||||
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -1,17 +1,18 @@
|
||||
name: Runner CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- releases/*
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
36
.github/workflows/codeql.yml
vendored
Normal file
36
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: "Code Scanning - Action"
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
|
||||
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
- name: Manual build
|
||||
run : |
|
||||
./dev.sh layout Release linux-x64
|
||||
working-directory: src
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
42
.github/workflows/release.yml
vendored
42
.github/workflows/release.yml
vendored
@@ -1,19 +1,20 @@
|
||||
name: Runner CD
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- releaseVersion
|
||||
|
||||
|
||||
jobs:
|
||||
check:
|
||||
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/master'
|
||||
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Make sure ./releaseVersion match ./src/runnerversion
|
||||
# Query GitHub release ensure version is not used
|
||||
# Make sure ./releaseVersion match ./src/runnerversion
|
||||
# Query GitHub release ensure version is not used
|
||||
- name: Check version
|
||||
uses: actions/github-script@0.3.0
|
||||
with:
|
||||
@@ -41,9 +42,15 @@ jobs:
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
build:
|
||||
needs: check
|
||||
outputs:
|
||||
linux-x64-sha: ${{ steps.sha.outputs.linux-x64-sha256 }}
|
||||
linux-arm64-sha: ${{ steps.sha.outputs.linux-arm64-sha256 }}
|
||||
linux-arm-sha: ${{ steps.sha.outputs.linux-arm-sha256 }}
|
||||
win-x64-sha: ${{ steps.sha.outputs.win-x64-sha256 }}
|
||||
osx-x64-sha: ${{ steps.sha.outputs.osx-x64-sha256 }}
|
||||
strategy:
|
||||
matrix:
|
||||
runtime: [ linux-x64, linux-arm64, linux-arm, win-x64, osx-x64 ]
|
||||
@@ -100,7 +107,19 @@ jobs:
|
||||
with:
|
||||
name: runner-packages
|
||||
path: _package
|
||||
|
||||
# compute shas and set as job outputs to use in release notes
|
||||
- run: brew install coreutils #needed for shasum util
|
||||
if: ${{ matrix.os == 'macOS-latest' }}
|
||||
name: Install Dependencies for SHA Calculation (osx)
|
||||
- run: |
|
||||
file=$(ls)
|
||||
sha=$(sha256sum $file | awk '{ print $1 }')
|
||||
echo "Computed sha256: $sha for $file"
|
||||
echo "::set-output name=${{matrix.runtime}}-sha256::$sha"
|
||||
shell: bash
|
||||
id: sha
|
||||
name: Compute SHA256
|
||||
working-directory: _package
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -125,11 +144,15 @@ jobs:
|
||||
const core = require('@actions/core')
|
||||
const fs = require('fs');
|
||||
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
|
||||
const releaseNote = fs.readFileSync('${{ github.workspace }}/releaseNote.md', 'utf8').replace(/<RUNNER_VERSION>/g, runnerVersion)
|
||||
var releaseNote = fs.readFileSync('${{ github.workspace }}/releaseNote.md', 'utf8').replace(/<RUNNER_VERSION>/g, runnerVersion)
|
||||
releaseNote = releaseNote.replace(/<WIN_X64_SHA>/g, '${{needs.build.outputs.win-x64-sha}}')
|
||||
releaseNote = releaseNote.replace(/<OSX_X64_SHA>/g, '${{needs.build.outputs.osx-x64-sha}}')
|
||||
releaseNote = releaseNote.replace(/<LINUX_X64_SHA>/g, '${{needs.build.outputs.linux-x64-sha}}')
|
||||
releaseNote = releaseNote.replace(/<LINUX_ARM_SHA>/g, '${{needs.build.outputs.linux-arm-sha}}')
|
||||
releaseNote = releaseNote.replace(/<LINUX_ARM64_SHA>/g, '${{needs.build.outputs.linux-arm64-sha}}')
|
||||
console.log(releaseNote)
|
||||
core.setOutput('version', runnerVersion);
|
||||
core.setOutput('note', releaseNote);
|
||||
|
||||
# Create GitHub release
|
||||
- uses: actions/create-release@master
|
||||
id: createRelease
|
||||
@@ -141,7 +164,6 @@ jobs:
|
||||
release_name: "v${{ steps.releaseNote.outputs.version }}"
|
||||
body: |
|
||||
${{ steps.releaseNote.outputs.note }}
|
||||
prerelease: true
|
||||
|
||||
# Upload release assets
|
||||
- name: Upload Release Asset (win-x64)
|
||||
@@ -192,4 +214,4 @@ jobs:
|
||||
upload_url: ${{ steps.createRelease.outputs.upload_url }}
|
||||
asset_path: ${{ github.workspace }}/actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_name: actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -8,10 +8,12 @@
|
||||
**/*.xproj
|
||||
**/*.xproj.user
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.error
|
||||
**/*.json.pretty
|
||||
.idea/
|
||||
.vscode
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
|
||||
# output
|
||||
node_modules
|
||||
@@ -22,7 +24,4 @@ _dotnetsdk
|
||||
TestResults
|
||||
TestLogs
|
||||
.DS_Store
|
||||
**/*.DotSettings.user
|
||||
|
||||
#generated
|
||||
src/Runner.Sdk/BuildConstants.cs
|
||||
**/*.DotSettings.user
|
||||
57
.vscode/launch.json
vendored
Normal file
57
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run [build]",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build runner layout",
|
||||
"program": "${workspaceFolder}/_layout/bin/Runner.Listener",
|
||||
"args": [
|
||||
"run"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/src",
|
||||
"console": "integratedTerminal",
|
||||
"requireExactSource": false,
|
||||
},
|
||||
{
|
||||
"name": "Run",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/_layout/bin/Runner.Listener",
|
||||
"args": [
|
||||
"run"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/src",
|
||||
"console": "integratedTerminal",
|
||||
"requireExactSource": false,
|
||||
},
|
||||
{
|
||||
"name": "Configure",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "create runner layout",
|
||||
"program": "${workspaceFolder}/_layout/bin/Runner.Listener",
|
||||
"args": [
|
||||
"configure"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/src",
|
||||
"console": "integratedTerminal",
|
||||
"requireExactSource": false,
|
||||
},
|
||||
{
|
||||
"name": "Debug Worker",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processName": "Runner.Worker",
|
||||
"requireExactSource": false,
|
||||
},
|
||||
{
|
||||
"name": "Attach Debugger",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}",
|
||||
"requireExactSource": false,
|
||||
},
|
||||
],
|
||||
}
|
||||
33
.vscode/tasks.json
vendored
Normal file
33
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "create runner layout",
|
||||
"detail": "Build and Copy all projects, scripts and external dependencies to _layout from src (run this the first time or after deleting _layout)",
|
||||
"command": "./dev.sh",
|
||||
"windows": {
|
||||
"command": "dev.cmd"
|
||||
},
|
||||
"args": [
|
||||
"layout"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "build runner layout",
|
||||
"detail": "Build and Copy all projects to _layout from src (run this on code change)",
|
||||
"command": "./dev.sh",
|
||||
"windows": {
|
||||
"command": "dev.cmd"
|
||||
},
|
||||
"args": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src"
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @actions/actions-runtime
|
||||
@@ -5,8 +5,9 @@
|
||||
# GitHub Actions Runner
|
||||
|
||||
[](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. The runner can run on the [hosted machine pools](https://github.com/actions/virtual-environments) or run on [self-hosted environments](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners).
|
||||
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.
|
||||
|
||||
## Get Started
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ADR 263: Self Hosted Runner Proxies
|
||||
# ADR 263: Self-Hosted Runner Proxies
|
||||
|
||||
**Date**: 2019-11-13
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
|
||||
## Context
|
||||
|
||||
- Proxy support is required for some enterprises and organizations to start using their own self hosted runners
|
||||
- While there is not a standard convention, many applications support setting proxies via the environmental variables `http_proxy`, `https_proxy`, `no_proxy`, such as curl, wget, perl, python, docker, git, R, ect
|
||||
- Proxy support is required for some enterprises and organizations to start using their own self-hosted runners
|
||||
- While there is not a standard convention, many applications support setting proxies via the environment variables `http_proxy`, `https_proxy`, `no_proxy`, such as curl, wget, perl, python, docker, git, and R
|
||||
- Some of these applications use `HTTPS_PROXY` versus `https_proxy`, but most understand or primarily support the lowercase variant
|
||||
|
||||
## Decision
|
||||
|
||||
We will update the Runner to use the conventional environment variables for proxies: `http_proxy`, `https_proxy` and `no_proxy` if they are set.
|
||||
We will update the Runner to use the conventional environment variables for proxies: `http_proxy`, `https_proxy`, and `no_proxy` if they are set.
|
||||
These are described in detail below:
|
||||
- `https_proxy` a proxy URL for all https traffic. It may contain basic authentication credentials. For example:
|
||||
- http://proxy.com
|
||||
@@ -22,20 +22,20 @@ These are described in detail below:
|
||||
- http://proxy.com
|
||||
- http://127.0.0.1:8080
|
||||
- http://user:password@proxy.com
|
||||
- `no_proxy` a comma seperated list of hosts that should not use the proxy. An optional port may be specified
|
||||
- `no_proxy` a comma-separated list of hosts that should not use the proxy. An optional port may be specified. For example:
|
||||
- `google.com`
|
||||
- `yahoo.com:443`
|
||||
- `google.com,bing.com`
|
||||
|
||||
We won't use `http_proxy` for https traffic when `https_proxy` is not set, this behavior lines up with any libcurl based tools (curl, git) and wget.
|
||||
Otherwise action authors and workflow users need to adjust to differences between the runner proxy convention, and tools used by their actions and scripts.
|
||||
Otherwise, action authors and workflow users need to adjust to differences between the runner proxy convention, and tools used by their actions and scripts.
|
||||
|
||||
Example:
|
||||
Customer set `http_proxy=http://127.0.0.1:8888` and configure the runner against `https://github.com/owner/repo`, with the `https_proxy` -> `http_proxy` fallback, the runner will connect to server without any problem. However, if user runs `git push` to `https://github.com/owner/repo`, `git` won't use the proxy since it require `https_proxy` to be set for any https traffic.
|
||||
Customer sets `http_proxy=http://127.0.0.1:8888` and configures the runner against `https://github.com/owner/repo`, with the `https_proxy` -> `http_proxy` fallback, the runner will connect to the server without any problem. However, if a user runs `git push` to `https://github.com/owner/repo`, `git` won't use the proxy since it requires `https_proxy` to be set for any https traffic.
|
||||
|
||||
> `golang`, `node.js` and other dev tools from the linux community use `http_proxy` for both http and https traffic base on my research.
|
||||
> `golang`, `node.js`, and other dev tools from the Linux community use `http_proxy` for both http and https traffic based on my research.
|
||||
|
||||
A majority of our users are using Linux where these variables are commonly required to be set by various programs. By reading these values, we simplify the process for self hosted runners to set up proxy, and expose it in a way users are already familiar with.
|
||||
A majority of our users are using Linux where these variables are commonly required to be set by various programs. By reading these values, we simplify the process for self-hosted runners to set up a proxy and expose it in a way users are already familiar with.
|
||||
|
||||
A password provided for a proxy will be masked in the logs.
|
||||
|
||||
@@ -43,19 +43,19 @@ We will support the lowercase and uppercase variants, with lowercase taking prio
|
||||
|
||||
### No Proxy Format
|
||||
|
||||
While exact implementations are different per application on handle `no_proxy` env, most applications accept a comma separated list of hosts. Some accept wildcard characters (*). We are going to do exact case-insentive matches, and not support wildcards at this time.
|
||||
While exact implementations are different per application on handling `no_proxy` env, most applications accept a comma-separated list of hosts. Some accept wildcard characters (`*`). We are going to do exact case-insensitive matches, and not support wildcards at this time.
|
||||
For example:
|
||||
- example.com will match example.com, foo.example.com, foo.bar.example.com
|
||||
- foo.example.com will match bar.foo.example.com and foo.example.com
|
||||
- `example.com` will match `example.com`, `foo.example.com`, and `foo.bar.example.com`
|
||||
- `foo.example.com` will match `bar.foo.example.com` and `foo.example.com`
|
||||
|
||||
We will not support IP addresses for `no_proxy`, only hostnames.
|
||||
|
||||
## Consequences
|
||||
|
||||
1. Enterprises and organizations needing proxy support will be able to embrace self hosted runners
|
||||
2. Users will need to set these environmental variables before configuring the runner in order to use a proxy when configuring
|
||||
3. The runner will read from the environmental variables during config and runtime and use the provided proxy if it exists
|
||||
4. Users may need to pass these environmental variables into other applications if they do not natively take these variables
|
||||
5. Action authors may need to update their workflows to react to the these environment variables
|
||||
6. We will document the way of setting environmental variables for runners using the environmental variables and how the runner uses them
|
||||
7. Like all other secrets, users will be able to relatively easily figure out proxy password if they can modify a workflow file running on a self hosted machine
|
||||
1. Enterprises and organizations needing proxy support will be able to embrace self-hosted runners
|
||||
2. Users will need to set these environment variables before configuring the runner in order to use a proxy when configuring
|
||||
3. The runner will read from the environment variables during config and runtime and use the provided proxy if it exists
|
||||
4. Users may need to pass these environment variables into other applications if they do not natively take these variables
|
||||
5. Action authors may need to update their workflows to react to these environment variables
|
||||
6. We will document the way of setting environment variables for runners using the environment variables and how the runner uses them
|
||||
7. Like all other secrets, users will be able to relatively easily figure out the proxy password if they can modify a workflow file running on a self-hosted machine
|
||||
|
||||
@@ -34,7 +34,7 @@ A way out for rare cases where scoping is a problem.
|
||||
|
||||
`##[remove-matcher]owner`
|
||||
|
||||
For the this to be usable, the `owner` needs to be discoverable. Therefore, debug print the owner on registration.
|
||||
For this to be usable, the `owner` needs to be discoverable. Therefore, debug print the owner on registration.
|
||||
|
||||
### Single line matcher
|
||||
|
||||
@@ -184,7 +184,7 @@ Solving this problem means:
|
||||
- Use the `github.workspace` (where the repo is cloned on disk)
|
||||
- Match against a repository to determine the relative path within the repo
|
||||
|
||||
This is a place where we diverge from VSCode. VSCode task configuration are specific to the local workspace (workspace root is known or can be specified). We're solving a more generic problem, so we need more information - specifically the `fromPath` property - in order to accurately root the path.
|
||||
This is a place where we diverge from VSCode. VSCode task configurations are specific to the local workspace (workspace root is known or can be specified). We're solving a more generic problem, so we need more information - specifically the `fromPath` property - in order to accurately root the path.
|
||||
|
||||
In order to avoid creating inaccurate hyperlinks on the error issues, the agent will verify the file exists and is in the main repository. Otherwise omit the file property from the error issue and debug trace what happened.
|
||||
|
||||
@@ -203,7 +203,7 @@ Problem matchers are unable to interpret severity strings other than `warning` a
|
||||
|
||||
However some tools indicate error/warning in different ways. For example `flake8` uses codes like `E100`, `W200`, and `F300` (error, warning, fatal, respectively).
|
||||
|
||||
Therefore, allow a property `severity`, sibling to `owner`, which identifies the default severity for the problem matcher. This allows two problem matchers are registered - one for warnings and one for errors.
|
||||
Therefore, allow a property `severity`, sibling to `owner`, which identifies the default severity for the problem matcher. This allows two problem matchers to be registered - one for warnings and one for errors.
|
||||
|
||||
For example, given the following `flake8` output:
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
run-actions run scripts using a platform specific shell:
|
||||
`bash -eo pipefail` on non-windows, and `cmd.exe /c /d /s` on windows
|
||||
|
||||
The `shell` option overwrites this to allow different flags or completely different shells/interpreters
|
||||
The `shell` option overrides this to allow different flags or completely different shells/interpreters
|
||||
|
||||
A small example is:
|
||||
```yml
|
||||
@@ -84,7 +84,7 @@ powershell/pwsh
|
||||
- Users can always opt out by not using the builtins, and providing a shell option like: `pwsh -File {0}`, or `powershell -Command "& '{0}'"`, depending on need
|
||||
|
||||
cmd
|
||||
- There doesnt seem to be a way to fully opt in to fail-fast behavior other than writing your script to check each error code and respond accordingly, so we cant actually provide that behavior by default, it will be completely up to the user to write this behavior into their script
|
||||
- There doesn't seem to be a way to fully opt in to fail-fast behavior other than writing your script to check each error code and respond accordingly, so we can't actually provide that behavior by default, it will be completely up to the user to write this behavior into their script
|
||||
- cmd.exe will exit (return the error code to the runner) with the errorlevel of the last program it executed. This is internally consistent with the previous default behavior (sh, pwsh) and is the cmd.exe default, so we keep that behavior
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
First party action `actions/cache` needs a input which is an explicit `key` used for restoring and saving the cache. For packages caching, the most comment `key` might be the hash result of contents from all `package-lock.json` under `node_modules` folder.
|
||||
First party action `actions/cache` needs a input which is an explicit `key` used for restoring and saving the cache. For packages caching, the most common `key` might be the hash result of contents from all `package-lock.json` under `node_modules` folder.
|
||||
|
||||
There are serval different ways to get the hash `key` input for `actions/cache` action.
|
||||
|
||||
@@ -38,7 +38,7 @@ There are serval different ways to get the hash `key` input for `actions/cache`
|
||||
`hashFiles()` will only support hashing files under the `$GITHUB_WORKSPACE` since the expression evaluated on the runner, if customer use job container or container action, the runner won't have access to file system inside the container.
|
||||
|
||||
`hashFiles()` will only take 1 parameters:
|
||||
- `hashFiles('**/package-lock.json')` // Search files under $GITHUB_WORKSPACE and calculate a hash for them
|
||||
- `hashFiles('**/package-lock.json')` // Search files under `$GITHUB_WORKSPACE` and calculate a hash for them
|
||||
|
||||
**Question: Do we need to support more than one match patterns?**
|
||||
Ex: `hashFiles('**/package-lock.json', '!toolkit/core/package-lock.json', '!toolkit/io/package-lock.json')`
|
||||
@@ -52,7 +52,7 @@ This will help customer has better experience with the `actions/cache` action's
|
||||
key: ${{hashFiles('**/package-lock.json')}}-${{github.ref}}-${{runner.os}}
|
||||
```
|
||||
|
||||
For search pattern, we will use basic globbing (`*` `?` and `[]`) and globstar (`**`).
|
||||
For search pattern, we will use basic globbing (`*`, `?`, and `[]`) and globstar (`**`).
|
||||
|
||||
Additional pattern details:
|
||||
- Root relative paths with `github.workspace` (the main repo)
|
||||
@@ -68,4 +68,4 @@ Hashing logic:
|
||||
5. Use SHA256 to hash all stored files' hash results to get the final 64 chars hash result.
|
||||
|
||||
**Question: Should we include the folder structure info into the hash?**
|
||||
Answer: No
|
||||
Answer: No
|
||||
|
||||
@@ -15,7 +15,7 @@ This gives us good coverage across the board for secrets and secrets with a pref
|
||||
|
||||
However, we don't have great coverage for cases where the secret has a string appended to it before it is base64 encoded (i.e.: `base64($pass\n))`).
|
||||
|
||||
Most notably we've seen this as a result of user error where a user accidentially appends a newline or space character before encoding their secret in base64.
|
||||
Most notably we've seen this as a result of user error where a user accidentally appends a newline or space character before encoding their secret in base64.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -45,4 +45,4 @@ This will result in us only revealing length or bit information when a prefix or
|
||||
|
||||
- In the case where a secret has a prefix or suffix added before base64 encoding, we may now reveal up to 20 bits of information and the length of the original string modulo 3, rather then the original 16 bits and no length information
|
||||
- Secrets with a suffix appended before encoding will now be masked across the board. Previously it was only masked if it was a multiple of 3 characters
|
||||
- Performance will suffer in a neglible way
|
||||
- Performance will suffer in a negligible way
|
||||
|
||||
75
docs/adrs/0361-wrapper-action.md
Normal file
75
docs/adrs/0361-wrapper-action.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# ADR 361: Wrapper Action
|
||||
|
||||
**Date**: 2020-03-06
|
||||
|
||||
**Status**: Pending
|
||||
|
||||
## Context
|
||||
|
||||
In addition to action's regular execution, action author may wants their action to have a chance to participate in:
|
||||
- Job initialization
|
||||
My Action will collect machine resource usage (CPU/RAM/Disk) during a workflow job execution, we need to start perf recorder at the beginning of the job.
|
||||
- Job cleanup
|
||||
My Action will dirty local workspace or machine environment during execution, we need to cleanup these changes at the end of the job.
|
||||
Ex: `actions/checkout@v2` will write `github.token` into local `.git/config` during execution, it has post job cleanup defined to undo the changes.
|
||||
|
||||
## Decision
|
||||
|
||||
### Add `pre` and `post` execution to action
|
||||
|
||||
Node Action Example:
|
||||
|
||||
```yaml
|
||||
name: 'My action with pre'
|
||||
description: 'My action with pre'
|
||||
runs:
|
||||
using: 'node12'
|
||||
pre: 'setup.js'
|
||||
pre-if: 'success()' // Optional
|
||||
main: 'index.js'
|
||||
post: 'cleanup.js'
|
||||
post-if: 'success()' // Optional
|
||||
```
|
||||
|
||||
Container Action Example:
|
||||
|
||||
```yaml
|
||||
name: 'My action with pre'
|
||||
description: 'My action with pre'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'mycontainer:latest'
|
||||
pre-entrypoint: 'setup.sh'
|
||||
pre-if: 'success()' // Optional
|
||||
entrypoint: 'entrypoint.sh'
|
||||
post-entrypoint: 'cleanup.sh'
|
||||
post-if: 'success()' // Optional
|
||||
```
|
||||
|
||||
Both `pre` and `post` will have default `pre-if/post-if` set to `always()`.
|
||||
Setting `pre` to `always()` will make sure no matter what condition evaluate result the `main` gets at runtime, the `pre` has always run already.
|
||||
`pre` executes in order of how the steps are defined.
|
||||
`pre` will always be added to job steps list during job setup.
|
||||
> Action referenced from local repository (`./my-action`) won't get `pre` setup correctly since the repository haven't checked-out during job initialization.
|
||||
> We can't use GitHub api to download the repository since there is about a 3 minute delay between `git push` and the new commit available to download using GitHub api.
|
||||
|
||||
`post` will be pushed into a `poststeps` stack lazily when the action's `pre` or `main` execution passed `if` condition check and about to run, you can't have an action that only contains a `post`, we will pop and run each `post` after all `pre` and `main` finished.
|
||||
> Currently `post` works for both repository action (`org/repo@v1`) and local action (`./my-action`)
|
||||
|
||||
Valid action:
|
||||
- only has `main`
|
||||
- has `pre` and `main`
|
||||
- has `main` and `post`
|
||||
- has `pre`, `main`, and `post`
|
||||
|
||||
Invalid action:
|
||||
- only has `pre`
|
||||
- only has `post`
|
||||
- has `pre` and `post`
|
||||
|
||||
Potential downside of introducing `pre`:
|
||||
|
||||
- Extra magic wrt step order. Users should control the step order. Especially when we introduce templates.
|
||||
- Eliminates the possibility to lazily download the action tarball, since `pre` always run by default, we have to download the tarball to check whether action defined a `pre`
|
||||
- `pre` doesn't work with local action, we suggested customer use local action for testing their action changes, ex CI for their action, to avoid delay between `git push` and GitHub repo tarball download api.
|
||||
- Condition on the `pre` can't be controlled using dynamic step outputs. `pre` executes too early.
|
||||
56
docs/adrs/0397-runner-registration-labels.md
Normal file
56
docs/adrs/0397-runner-registration-labels.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# ADR 0397: Support adding custom labels during runner config
|
||||
**Date**: 2020-03-30
|
||||
|
||||
**Status**: Approved
|
||||
|
||||
## Context
|
||||
|
||||
Since configuring self-hosted runners is commonly automated via scripts, the labels need to be able to be created during configuration. The runner currently registers the built-in labels (os, arch) during registration but does not accept labels via command line args to extend the set registered.
|
||||
|
||||
See Issue: https://github.com/actions/runner/issues/262
|
||||
|
||||
This is another version of [ADR275](https://github.com/actions/runner/pull/275)
|
||||
|
||||
## Decision
|
||||
|
||||
This ADR proposes that we add a `--labels` option to the `config`, which could be used to add custom additional labels to the configured runner.
|
||||
|
||||
For example, to add a single additional label the operator could run:
|
||||
```bash
|
||||
./config.sh --labels mylabel
|
||||
```
|
||||
> Note: the current runner command line parsing and envvar override algorithm only support a single argument (key).
|
||||
|
||||
This would add the label `mylabel` to the runner, and enable users to select the runner in their workflow using this label:
|
||||
```yaml
|
||||
runs-on: [self-hosted, mylabel]
|
||||
```
|
||||
|
||||
To add multiple labels the operator could run:
|
||||
```bash
|
||||
./config.sh --labels mylabel,anotherlabel
|
||||
```
|
||||
> Note: the current runner command line parsing and envvar override algorithm only supports a single argument (key).
|
||||
|
||||
This would add the label `mylabel` and `anotherlabel` to the runner, and enable users to select the runner in their workflow using this label:
|
||||
```yaml
|
||||
runs-on: [self-hosted, mylabel, anotherlabel]
|
||||
```
|
||||
|
||||
It would not be possible to remove labels from an existing runner using `config.sh`, instead labels would have to be removed using the GitHub UI.
|
||||
|
||||
The labels argument will split on commas, trim and discard empty strings. That effectively means don't use commas in unattended config label names. Alternatively, we could choose to escape commas but it's a nice to have.
|
||||
|
||||
## Replace
|
||||
|
||||
If an existing runner exists and the option to replace is chosen (interactively or via unattended as in this scenario), then the labels will be replaced/overwritten (not merged).
|
||||
|
||||
## Overriding built-in labels
|
||||
|
||||
Note that it is possible to register "built-in" hosted labels like `ubuntu-latest` and is not considered an error. This is an effective way for the org/runner admin to dictate by policy through registration that this set of runners will be used without having to edit all the workflow files now and in the future.
|
||||
|
||||
We will also not make other restrictions such as limiting explicitly adding os/arch labels and validating. We will assume that explicit labels were added for a reason and not restricting offers the most flexibility and future-proofing / compatibility.
|
||||
|
||||
## Consequences
|
||||
|
||||
The ability to add custom labels to a self-hosted runner would enable most scenarios where job runner selection based on runner capabilities or characteristics are required.
|
||||
378
docs/adrs/0549-composite-run-steps.md
Normal file
378
docs/adrs/0549-composite-run-steps.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# ADR 0549: Composite Run Steps
|
||||
|
||||
**Date**: 2020-06-17
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Customers want to be able to compose actions from actions (ex: https://github.com/actions/runner/issues/438)
|
||||
|
||||
An important step towards meeting this goal is to build functionality for actions where users can simply execute any number of steps.
|
||||
|
||||
### Guiding Principles
|
||||
|
||||
We don't want the workflow author to need to know how the internal workings of the action work. Users shouldn't know the internal workings of the composite action (for example, `default.shell` and `default.workingDir` should not be inherited from the workflow file to the action file). When deciding how to design certain parts of composite run steps, we want to treat it as one logical step for the consumer.
|
||||
|
||||
A composite action is treated as **one** individual job step (this is known as encapsulation).
|
||||
|
||||
## Decision
|
||||
|
||||
**In this ADR, we only support running multiple run steps in an Action.** In doing so, we build in support for mapping and flowing the inputs, outputs, and env variables (ex: All nested steps should have access to their parents' input variables and nested steps can overwrite the input variables).
|
||||
|
||||
### Composite Run Steps Features
|
||||
This feature supports at the top action level:
|
||||
- name
|
||||
- description
|
||||
- inputs
|
||||
- runs
|
||||
- outputs
|
||||
|
||||
This feature supports at the run step level:
|
||||
- name
|
||||
- id
|
||||
- run
|
||||
- env
|
||||
- shell
|
||||
- working-directory
|
||||
|
||||
This feature **does not support** at the run step level:
|
||||
- timeout-minutes
|
||||
- secrets
|
||||
- conditionals (needs, if, etc.)
|
||||
- continue-on-error
|
||||
|
||||
### Steps
|
||||
|
||||
Example `workflow.yml`
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- id: step1
|
||||
uses: actions/setup-python@v1
|
||||
- id: step2
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v2
|
||||
- uses: user/composite@v1
|
||||
- name: workflow step 1
|
||||
run: echo hello world 3
|
||||
- name: workflow step 2
|
||||
run: echo hello world 4
|
||||
```
|
||||
|
||||
Example `user/composite/action.yml`
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: pip install -r requirements.txt
|
||||
shell: bash
|
||||
- run: npm install
|
||||
shell: bash
|
||||
```
|
||||
|
||||
Example Output
|
||||
|
||||
```yaml
|
||||
[npm installation output]
|
||||
[pip requirements output]
|
||||
echo hello world 3
|
||||
echo hello world 4
|
||||
```
|
||||
|
||||
We add a token called "composite" which allows our Runner code to process composite actions. By invoking "using: composite", our Runner code then processes the "steps" attribute, converts this template code to a list of steps, and finally runs each run step sequentially. If any step fails and there are no `if` conditions defined, the whole composite action job fails.
|
||||
|
||||
### Defaults
|
||||
|
||||
We will not support "defaults" in a composite action.
|
||||
|
||||
### Shell and Working-directory
|
||||
|
||||
For each run step in a composite action, the action author can set the `shell` and `working-directory` attributes for that step. The shell attribute is **required** for each run step because the action author does not know what the workflow author is using for the operating system so we need to explicitly prevent unknown behavior by making sure that each run step has an explicit shell **set by the action author.** On the other hand, `working-directory` is optional. Moreover, the composite action author can map in values from the `inputs` for its `shell` and `working-directory` attributes at the step level for an action.
|
||||
|
||||
For example,
|
||||
|
||||
`action.yml`
|
||||
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
shell_1:
|
||||
description: 'Your name'
|
||||
default: 'pwsh'
|
||||
steps:
|
||||
- run: echo 1
|
||||
shell: ${{ inputs.shell_1 }}
|
||||
```
|
||||
|
||||
Note, the workflow file and action file are treated as separate entities. **So, the workflow `defaults` will never change the `shell` and `working-directory` value in the run steps in a composite action.** Note, `defaults` in a workflow only apply to run steps not "uses" steps (steps that use an action).
|
||||
|
||||
### Running Local Scripts
|
||||
|
||||
Example 'workflow.yml':
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: user/composite@v1
|
||||
```
|
||||
|
||||
Example `user/composite/action.yml`:
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: chmod +x ${{ github.action_path }}/test/script2.sh
|
||||
shell: bash
|
||||
- run: chmod +x $GITHUB_ACTION_PATH/script.sh
|
||||
shell: bash
|
||||
- run: ${{ github.action_path }}/test/script2.sh
|
||||
shell: bash
|
||||
- run: $GITHUB_ACTION_PATH/script.sh
|
||||
shell: bash
|
||||
```
|
||||
Where `user/composite` has the file structure:
|
||||
```
|
||||
.
|
||||
+-- action.yml
|
||||
+-- script.sh
|
||||
+-- test
|
||||
| +-- script2.sh
|
||||
```
|
||||
|
||||
|
||||
Users will be able to run scripts located in their action folder by first prepending the relative path and script name with `$GITHUB_ACTION_PATH` or `github.action_path` which contains the path in which the composite action is downloaded to and where those "files" live. Note, you'll have to use `chmod` before running each script if you do not git check in your script files into your github repo with the executable bit turned on.
|
||||
|
||||
### Inputs
|
||||
|
||||
Example `workflow.yml`:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- id: foo
|
||||
uses: user/composite@v1
|
||||
with:
|
||||
your_name: "Octocat"
|
||||
```
|
||||
|
||||
Example `user/composite/action.yml`:
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
your_name:
|
||||
description: 'Your name'
|
||||
default: 'Ethan'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: echo hello ${{ inputs.your_name }}
|
||||
shell: bash
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```
|
||||
hello Octocat
|
||||
```
|
||||
|
||||
Each input variable in the composite action is only viewable in its own scope.
|
||||
|
||||
### Outputs
|
||||
|
||||
Example `workflow.yml`:
|
||||
|
||||
```yaml
|
||||
...
|
||||
steps:
|
||||
- id: foo
|
||||
uses: user/composite@v1
|
||||
- run: echo random-number ${{ steps.foo.outputs.random-number }}
|
||||
shell: bash
|
||||
```
|
||||
|
||||
Example `user/composite/action.yml`:
|
||||
|
||||
```yaml
|
||||
outputs:
|
||||
random-number:
|
||||
description: "Random number"
|
||||
value: ${{ steps.random-number-generator.outputs.random-id }}
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- id: random-number-generator
|
||||
run: echo "::set-output name=random-id::$(echo $RANDOM)"
|
||||
shell: bash
|
||||
```
|
||||
|
||||
Example Output:
|
||||
|
||||
```
|
||||
::set-output name=my-output::43243
|
||||
random-number 43243
|
||||
```
|
||||
|
||||
Each of the output variables from the composite action is viewable from the workflow file that uses the composite action. In other words, every child's action output(s) are only viewable by its parent using dot notation (ex `steps.foo.outputs.random-number`).
|
||||
|
||||
Moreover, the output ids are only accessible within the scope where it was defined. Note that in the example above, in our `workflow.yml` file, it should not have access to output id (i.e. `random-id`). The reason why we are doing this is that we don't want to require the workflow author to know the internal workings of the composite action.
|
||||
|
||||
### Context
|
||||
|
||||
Similar to the workflow file, the composite action has access to the [same context objects](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts) (ex: `github`, `env`, `strategy`).
|
||||
|
||||
### Environment
|
||||
|
||||
In the Composite Action, you'll only be able to use `::set-env::` to set environment variables just like you could with other actions.
|
||||
|
||||
### Secrets
|
||||
|
||||
**We will not support "Secrets" in a composite action for now. This functionality will be focused on in a future ADR.**
|
||||
|
||||
We'll pass the secrets from the composite action's parents (ex: the workflow file) to the composite action. Secrets can be created in the composite action with the secrets context. In the actions yaml, we'll automatically mask the secret.
|
||||
|
||||
|
||||
### If-Condition
|
||||
|
||||
** `If` and `needs` conditions will not be supported in the composite run steps feature. It will be supported later on in a new feature. **
|
||||
|
||||
Old reasoning:
|
||||
|
||||
Example `workflow.yml`:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- run: exit 1
|
||||
- uses: user/composite@v1 # <--- this will run, as it's marked as always running
|
||||
if: always()
|
||||
```
|
||||
|
||||
Example `user/composite/action.yml`:
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: echo "just succeeding"
|
||||
shell: bash
|
||||
- run: echo "I will run, as my current scope is succeeding"
|
||||
shell: bash
|
||||
if: success()
|
||||
- run: exit 1
|
||||
shell: bash
|
||||
- run: echo "I will not run, as my current scope is now failing"
|
||||
shell: bash
|
||||
```
|
||||
|
||||
**We will not support "if-condition" in a composite action for now. This functionality will be focused on in a future ADR.**
|
||||
|
||||
See the paragraph below for a rudimentary approach (thank you to @cybojenix for the idea, example, and explanation for this approach):
|
||||
|
||||
The `if` statement in the parent (in the example above, this is the `workflow.yml`) shows whether or not we should run the composite action. So, our composite action will run since the `if` condition for running the composite action is `always()`.
|
||||
|
||||
**Note that the "if-condition" on the parent does not propagate to the rest of its children though.**
|
||||
|
||||
In the child action (in this example, this is the `action.yml`), it starts with a clean slate (in other words, no imposing if-conditions). Similar to the logic in the paragraph above, `echo "I will run, as my current scope is succeeding"` will run since the `if` condition checks if the previous steps **within this composite action** have not failed. `run: echo "I will not run, as my current scope is now failing"` will not run since the previous step resulted in an error and by default, the if expression is set to `success()` if the if-condition is not set for a step.
|
||||
|
||||
|
||||
What if a step has `cancelled()`? We do the opposite of our approach above if `cancelled()` is used for any of our composite run steps. We will cancel any step that has this condition if the workflow is cancelled at all.
|
||||
|
||||
### Timeout-minutes
|
||||
|
||||
Example `workflow.yml`:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- id: bar
|
||||
uses: user/test@v1
|
||||
timeout-minutes: 50
|
||||
```
|
||||
|
||||
Example `user/composite/action.yml`:
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- id: foo1
|
||||
run: echo test 1
|
||||
timeout-minutes: 10
|
||||
shell: bash
|
||||
- id: foo2
|
||||
run: echo test 2
|
||||
shell: bash
|
||||
- id: foo3
|
||||
run: echo test 3
|
||||
timeout-minutes: 10
|
||||
shell: bash
|
||||
```
|
||||
|
||||
**We will not support "timeout-minutes" in a composite action for now. This functionality will be focused on in a future ADR.**
|
||||
|
||||
A composite action in its entirety is a job. You can set both timeout-minutes for the whole composite action or its steps as long as the sum of the `timeout-minutes` for each composite action step that has the attribute `timeout-minutes` is less than or equals to `timeout-minutes` for the composite action. There is no default timeout-minutes for each composite action step.
|
||||
|
||||
If the time taken for any of the steps in combination or individually exceeds the whole composite action `timeout-minutes` attribute, the whole job will fail (1). If an individual step exceeds its own `timeout-minutes` attribute but the total time that has been used including this step is below the overall composite action `timeout-minutes`, the individual step will fail but the rest of the steps will run based on their own `timeout-minutes` attribute (they will still abide by condition (1) though).
|
||||
|
||||
For reference, in the example above, if the composite step `foo1` takes 11 minutes to run, that step will fail but the rest of the steps, `foo1` and `foo2`, will proceed as long as their total runtime with the previous failed `foo1` action is less than the composite action's `timeout-minutes` (50 minutes). If the composite step `foo2` takes 51 minutes to run, it will cause the whole composite action job to fail.
|
||||
|
||||
The rationale behind this is that users can configure their steps with the `if` condition to conditionally set how steps rely on each other. Due to the additional capabilities that are offered with combining `timeout-minutes` and/or `if`, we wanted the `timeout-minutes` condition to be as dumb as possible and not affect other steps.
|
||||
|
||||
[Usage limits still apply](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions?query=if%28%29#usage-limits)
|
||||
|
||||
|
||||
### Continue-on-error
|
||||
|
||||
Example `workflow.yml`:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- run: exit 1
|
||||
- id: bar
|
||||
uses: user/test@v1
|
||||
continue-on-error: false
|
||||
- id: foo
|
||||
run: echo "Hello World" <------- This step will not run
|
||||
```
|
||||
|
||||
Example `user/composite/action.yml`:
|
||||
|
||||
```yaml
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- run: exit 1
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
- run: echo "Hello World 2" <----- This step will run
|
||||
shell: bash
|
||||
```
|
||||
|
||||
**We will not support "continue-on-error" in a composite action for now. This functionality will be focused on in a future ADR.**
|
||||
|
||||
If any of the steps fail in the composite action and the `continue-on-error` is set to `false` for the whole composite action step in the workflow file, then the steps below it will run. On the flip side, if `continue-on-error` is set to `true` for the whole composite action step in the workflow file, the next job step will run.
|
||||
|
||||
For the composite action steps, it follows the same logic as above. In this example, `"Hello World 2"` will be outputted because the previous step has `continue-on-error` set to `true` although that previous step errored.
|
||||
|
||||
### Visualizing Composite Action in the GitHub Actions UI
|
||||
We want all the composite action's steps to be condensed into the original composite action node.
|
||||
|
||||
Here is a visual representation of the [first example](#Steps)
|
||||
|
||||
```yaml
|
||||
| composite_action_node |
|
||||
| echo hello world 1 |
|
||||
| echo hello world 2 |
|
||||
| echo hello world 3 |
|
||||
| echo hello world 4 |
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
This ADR lays the framework for eventually supporting nested Composite Actions within Composite Actions. This ADR allows for users to run multiple run steps within a GitHub Composite Action with the support of inputs, outputs, environment, and context for use in any steps as well as the if, timeout-minutes, and the continue-on-error attributes for each Composite Action step.
|
||||
92
docs/adrs/1144-composite-actions.md
Normal file
92
docs/adrs/1144-composite-actions.md
Normal file
@@ -0,0 +1,92 @@
|
||||
**Date**: 2021-06-10
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
We released [composite run steps](https://github.com/actions/runner/pull/554) last year which started our journey of reusing steps across different workflow files. To continue that journey, we want to expand composite run steps into composite actions.
|
||||
|
||||
We want to support the `uses` steps from workflows in composite actions, including:
|
||||
- Container actions
|
||||
- Javascript actions
|
||||
- Other Composite actions (up to a limit of course!)
|
||||
- The pre and post steps these actions can generate
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
- Composite Actions should function as a single step or action, no matter how many steps it is composed of or how many levels of recursion it has
|
||||
- In the future we may add a configurable option to make this no longer the case
|
||||
- A workflow author should not need to understand the inner workings of a composite action in order to use it
|
||||
- Composite actions should leverage inputs to get values they need, they will not have full access to the `context` objects. The secrets context will **not** be available to composite actions, users will need to pass these values in as an input.
|
||||
- Other Actions should **just work** inside a composite action, without any code changes
|
||||
|
||||
## Decisions
|
||||
|
||||
### Composite Recursion Limit
|
||||
|
||||
- We will start with supporting a recursion limit of `10` composite actions deep
|
||||
- We are free to bump this limit in the future, the code will be written to just require updating a variable. If the graph evaluates beyond the recursion limit, the job will fail in the pre-job phase (The `Set up job` step).
|
||||
- A composite actions interface is its inputs and outputs, nothing else is carried over when invoking recursively.
|
||||
|
||||
### Pre/Post Steps in nested Actions
|
||||
|
||||
- We do not plan on adding the ability to configure a customizable pre or post step for composite actions at this time. However, we will execute the pre and post steps of any actions referenced in a composite action.
|
||||
- Composite actions will generate a single pre-step and post-step for the entire composite action, even if there are multiple pre-steps and post-steps in the referenced actions.
|
||||
- These steps will execute following the same ordering rules we have today, first to run has their pre step run first and their post step run last.
|
||||
- For example, if you had a composite action with two pre steps and two posts steps:
|
||||
|
||||
```
|
||||
- uses: action1
|
||||
- uses: composite1
|
||||
- uses: action2
|
||||
```
|
||||
|
||||
The order of execution would be:
|
||||
|
||||
```
|
||||
- prestep-action1
|
||||
- prestep-composite1
|
||||
- prestep-composite1-first-action-referenced
|
||||
- prestep-composite1-second-action-referenced
|
||||
- prestep-action2
|
||||
- the job steps
|
||||
- poststep-action2
|
||||
- poststep-composite1
|
||||
- poststep-composite1-the-second-action-referenced
|
||||
- poststep-composite1-first-action-referenced
|
||||
- poststep-action1
|
||||
```
|
||||
|
||||
#### Set-state
|
||||
|
||||
- While the composite action has an individual combined pre/post action, the `set-state` command will not be shared.
|
||||
- If the `set-state` command is used during a composite step, only the action that originally called `set-state` will have access to the env variable during the post run step.
|
||||
- This prevents multiple actions that set the same state from interfering with the execution of another action's post step.
|
||||
|
||||
### Resolve Action Endpoint changes
|
||||
|
||||
- The resolve actions endpoint will now validate policy to ensure that the given workflow run has access to download that action.
|
||||
- Older GHES/GHAE customers with newer runners will be locked out of composite uses steps until they upgrade their instance.
|
||||
|
||||
### Local actions
|
||||
- Local actions will expand the tree, perform policy checks, and download actions Just in Time when the step is running.
|
||||
- Like current local actions, we will not support presteps. If an action is running local, by the time we know that, the time to run presteps have already passed.
|
||||
|
||||
### If, continue-on-error, timeout-minutes - Not being considered at this time
|
||||
|
||||
- `if`, `continue-on-error`, `timeout-minutes` could be supported in composite run/uses steps. These values were not originally supported in our composite run steps implementation.
|
||||
- Browsing the community forums and runner repo, there hasn't been a lot of noise asking for these features, so we will hold off on them.
|
||||
- These values passed as input into the composite action will **not** be carried over as input into the individual steps the composite action runs.
|
||||
|
||||
### Defaults - Not being considered at this time
|
||||
|
||||
- In actions, we have the idea of [defaults](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#defaultsrun) , which allow you to specify a shell and working directory in one location, rather then on each step.
|
||||
- However, `shell` is currently required in composite run steps
|
||||
- In regular run steps, it is optional, and defaults to a different value based on the OS.
|
||||
- We want to prioritize the right experience for the consumer, and make the action author continue to explicitly set these values. We can consider improving this experience in the future.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Workflows are now more reusable across multiple workflow files
|
||||
- Composite actions implement most of the existing workflow run steps, with room to expand these in the future
|
||||
- Feature flags will control this rollout
|
||||
71
docs/adrs/1438-conditional-composite.md
Normal file
71
docs/adrs/1438-conditional-composite.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# ADR 1438: Support Conditionals In Composite Actions
|
||||
|
||||
**Date**: 2021-10-13
|
||||
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
We recently shipped composite actions, which allows you to reuse individual steps inside an action.
|
||||
However, one of the [most requested features](https://github.com/actions/runner/issues/834) has been a way to support the `if` keyword.
|
||||
|
||||
### Goals
|
||||
- We want to keep consistent with current behavior
|
||||
- We want to support conditionals via the `if` keyword
|
||||
- Our built in functions like `success` should be implementable without calling them, for example you can do `job.status == success` rather then `success()` currently.
|
||||
|
||||
### How does composite currently work?
|
||||
|
||||
Currently, we have limited conditional support in composite actions for `pre` and `post` steps.
|
||||
These are based on the `job status`, and support keywords like `always()`, `failed()`, `success()` and `cancelled()`.
|
||||
However, generic or main steps do **not** support conditionals.
|
||||
|
||||
By default, in a regular workflow, a step runs on the `success()` condition. Which looks at the **job** **status**, sees if it is successful and runs.
|
||||
|
||||
By default, in a composite action, main steps run until a single step fails in that composite action, then the composite action is halted early. It does **not** care about the job status.
|
||||
Pre, and post steps in composite actions use the job status to determine if they should run.
|
||||
|
||||
### How do we go forward?
|
||||
|
||||
Well, if we think about what composite actions are currently doing when invoking main steps, they are checking if the current composite action is successful.
|
||||
Lets formalize that concept into a "real" idea.
|
||||
|
||||
- We will add an `action_status` field to the github context to mimic the [job's context status](https://docs.github.com/en/actions/learn-github-actions/contexts#job-context).
|
||||
- We have an existing concept that does this `action_path` which is only set for composite actions on the github context.
|
||||
- In a composite action during a main step, the `success()` function will check if `action_status == success`, rather then `job_status == success`. Failure will work the same way.
|
||||
- Pre and post steps in composite actions will not change, they will continue to check the job status.
|
||||
|
||||
|
||||
### Nested Scenario
|
||||
For nested composite actions, we will follow the existing behavior, you only care about your current composite action, not any parents.
|
||||
For example, lets imagine a scenario with a simple nested composite action
|
||||
|
||||
```
|
||||
- Job
|
||||
- Regular Step
|
||||
- Composite Action
|
||||
- runs: exit 1
|
||||
- if: always()
|
||||
uses: A child composite action
|
||||
- if: success()
|
||||
runs: echo "this should print"
|
||||
- runs: echo "this should also print"
|
||||
- if: success()
|
||||
runs: echo "this will not print as the current composite action has failed already"
|
||||
|
||||
```
|
||||
The child composite actions steps should run in this example, the child composite action has not yet failed, so it should run all steps until a step fails. This is consistent with how a composite action currently works in production if the main job fails but a composite action is invoked with `if:always()` or `if: failure()`
|
||||
|
||||
### Other options explored
|
||||
We could add the `current_step_status` to the job context rather then `__status` to the steps context, however this comes with two major downsides:
|
||||
- We need to support the field for every type of step, because its non trivial to remove a field from the job context once it has been added (its readonly)
|
||||
- For all actions besides composite it would only every be `success`
|
||||
- Its weird to have a `current_step` value on the job context
|
||||
- We also explored a `__status` on the steps context.
|
||||
- The `__` is required to prevent us from colliding with a step with id: status
|
||||
- This felt wrong because the naming was not smooth, and did not fit into current conventions.
|
||||
|
||||
### Consequences
|
||||
- github context has a new field for the status of the current composite action.
|
||||
- We support conditional's in composite actions
|
||||
- We keep the existing behavior for all users, but allow them to expand that functionality.
|
||||
78
docs/automate.md
Normal file
78
docs/automate.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Automate Configuring Self-Hosted Runners
|
||||
|
||||
|
||||
## Export PAT
|
||||
|
||||
Before running any of these sample scripts, create a GitHub PAT and export it before running the script
|
||||
|
||||
```bash
|
||||
export RUNNER_CFG_PAT=yourPAT
|
||||
```
|
||||
|
||||
## Create running as a service
|
||||
|
||||
**Scenario**: Run on a machine or VM ([not container](#why-cant-i-use-a-container)) which automates:
|
||||
|
||||
- Resolving latest released runner
|
||||
- Download and extract latest
|
||||
- Acquire a registration token
|
||||
- Configure the runner
|
||||
- Run as a systemd (linux) or Launchd (osx) service
|
||||
|
||||
:point_right: [Sample script here](../scripts/create-latest-svc.sh) :point_left:
|
||||
|
||||
Run as a one-liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level)
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/actions/runner/main/scripts/create-latest-svc.sh | bash -s yourorg/yourrepo
|
||||
```
|
||||
|
||||
You can call the script with additional arguments:
|
||||
```bash
|
||||
# Usage:
|
||||
# export RUNNER_CFG_PAT=<yourPAT>
|
||||
# ./create-latest-svc -s scope -g [ghe_domain] -n [name] -u [user] -l [labels]
|
||||
# -s required scope: repo (:owner/:repo) or org (:organization)
|
||||
# -g optional ghe_hostname: the fully qualified domain name of your GitHub Enterprise Server deployment
|
||||
# -n optional name of the runner, defaults to hostname
|
||||
# -u optional user svc will run as, defaults to current
|
||||
# -l optional list of labels (split by comma) applied on the runner"
|
||||
```
|
||||
|
||||
Use `--` to pass any number of optional named parameters:
|
||||
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/actions/runner/main/scripts/create-latest-svc.sh | bash -s -- -s myorg/myrepo -n myname -l label1,label2
|
||||
```
|
||||
### Why can't I use a container?
|
||||
|
||||
The runner is installed as a service using `systemd` and `systemctl`. Docker does not support `systemd` for service configuration on a container.
|
||||
|
||||
## Uninstall running as service
|
||||
|
||||
**Scenario**: Run on a machine or VM ([not container](#why-cant-i-use-a-container)) which automates:
|
||||
|
||||
- Stops and uninstalls the systemd (linux) or Launchd (osx) service
|
||||
- Acquires a removal token
|
||||
- Removes the runner
|
||||
|
||||
:point_right: [Sample script here](../scripts/remove-svc.sh) :point_left:
|
||||
|
||||
Repo level one liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level)
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/actions/runner/main/scripts/remove-svc.sh | bash -s yourorg/yourrepo
|
||||
```
|
||||
|
||||
### Delete an offline runner
|
||||
|
||||
**Scenario**: Deletes a registered runner that is offline:
|
||||
|
||||
- Ensures the runner is offline
|
||||
- Resolves id from name
|
||||
- Deletes the runner
|
||||
|
||||
:point_right: [Sample script here](../scripts/delete.sh) :point_left:
|
||||
|
||||
Repo level one-liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level) and replace runnername
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/actions/runner/main/scripts/delete.sh | bash -s yourorg/yourrepo runnername
|
||||
```
|
||||
45
docs/checks/actions.md
Normal file
45
docs/checks/actions.md
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
# Actions Connection Check
|
||||
|
||||
## What is this check for?
|
||||
|
||||
Make sure the runner has access to actions service for GitHub.com or GitHub Enterprise Server
|
||||
|
||||
- For GitHub.com
|
||||
- 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://pipelines.actions.githubusercontent.com/_apis/.../ for receiving workflow jobs.
|
||||
- For GitHub Enterprise Server
|
||||
- The runner needs to access https://myGHES.com/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://myGHES.com/_services/pipelines/_apis/.../ for receiving workflow jobs.
|
||||
|
||||
## What is checked?
|
||||
|
||||
- DNS lookup for api.github.com or myGHES.com using dotnet
|
||||
- Ping api.github.com or myGHES.com using dotnet
|
||||
- Make HTTP GET to https://api.github.com or https://myGHES.com/api/v3 using dotnet, check response headers contains `X-GitHub-Request-Id`
|
||||
---
|
||||
- DNS lookup for vstoken.actions.githubusercontent.com using dotnet
|
||||
- Ping vstoken.actions.githubusercontent.com using dotnet
|
||||
- Make HTTP GET to https://vstoken.actions.githubusercontent.com/_apis/health or https://myGHES.com/_services/vstoken/_apis/health using dotnet, check response headers contains `x-vss-e2eid`
|
||||
---
|
||||
- DNS lookup for pipelines.actions.githubusercontent.com using dotnet
|
||||
- Ping pipelines.actions.githubusercontent.com using dotnet
|
||||
- Make HTTP GET to https://pipelines.actions.githubusercontent.com/_apis/health or https://myGHES.com/_services/pipelines/_apis/health using dotnet, check response headers contains `x-vss-e2eid`
|
||||
- Make HTTP POST to https://pipelines.actions.githubusercontent.com/_apis/health or https://myGHES.com/_services/pipelines/_apis/health using dotnet, check response headers contains `x-vss-e2eid`
|
||||
|
||||
## How to fix the issue?
|
||||
|
||||
### 1. Check the common network issue
|
||||
|
||||
> Please check the [network doc](./network.md)
|
||||
|
||||
### 2. SSL certificate related issue
|
||||
|
||||
If you are seeing `System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.` in the log, it means the runner can't connect to Actions service due to SSL handshake failure.
|
||||
> Please check the [SSL cert doc](./sslcert.md)
|
||||
|
||||
## 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.
|
||||
34
docs/checks/git.md
Normal file
34
docs/checks/git.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Git Connection Check
|
||||
|
||||
## What is this check for?
|
||||
|
||||
Make sure `git` can access GitHub.com or your GitHub Enterprise Server.
|
||||
|
||||
|
||||
## What is checked?
|
||||
|
||||
The test is done by executing
|
||||
```bash
|
||||
# For GitHub.com
|
||||
git ls-remote --exit-code https://github.com/actions/checkout HEAD
|
||||
|
||||
# For GitHub Enterprise Server
|
||||
git ls-remote --exit-code https://ghes.me/actions/checkout HEAD
|
||||
```
|
||||
|
||||
The test also set environment variable `GIT_TRACE=1` and `GIT_CURL_VERBOSE=1` before running `git ls-remote`, this will make `git` to produce debug log for better debug any potential issues.
|
||||
|
||||
## How to fix the issue?
|
||||
|
||||
### 1. Check the common network issue
|
||||
|
||||
> Please check the [network doc](./network.md)
|
||||
|
||||
### 2. SSL certificate related issue
|
||||
|
||||
If you are seeing `SSL Certificate problem:` in the log, it means the `git` can't connect to the GitHub server due to SSL handshake failure.
|
||||
> Please check the [SSL cert doc](./sslcert.md)
|
||||
|
||||
## 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.
|
||||
26
docs/checks/internet.md
Normal file
26
docs/checks/internet.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Internet Connection Check
|
||||
|
||||
## What is this check for?
|
||||
|
||||
Make sure the runner has access to https://api.github.com
|
||||
|
||||
The runner needs to access https://api.github.com to download any actions from the marketplace.
|
||||
|
||||
Even the runner is configured to GitHub Enterprise Server, the runner can still download actions from GitHub.com with [GitHub Connect](https://docs.github.com/en/enterprise-server@2.22/admin/github-actions/enabling-automatic-access-to-githubcom-actions-using-github-connect)
|
||||
|
||||
|
||||
## What is checked?
|
||||
|
||||
- DNS lookup for api.github.com using dotnet
|
||||
- Ping api.github.com using dotnet
|
||||
- Make HTTP GET to https://api.github.com using dotnet, check response headers contains `X-GitHub-Request-Id`
|
||||
|
||||
## How to fix the issue?
|
||||
|
||||
### 1. Check the common network issue
|
||||
|
||||
> Please check the [network doc](./network.md)
|
||||
|
||||
## 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.
|
||||
62
docs/checks/network.md
Normal file
62
docs/checks/network.md
Normal file
@@ -0,0 +1,62 @@
|
||||
## Common Network Related Issues
|
||||
|
||||
### Common things that can cause the runner to not working properly
|
||||
|
||||
- A bug in the runner or the dotnet framework that causes the actions runner to be unable to make Http requests in a certain network environment.
|
||||
|
||||
- A Proxy or Firewall may block certain HTTP method, such as blocking all POST and PUT calls which the runner will use to upload logs.
|
||||
|
||||
- A Proxy or Firewall may only allows requests with certain user-agent to pass through and the actions runner user-agent is not in the allow list.
|
||||
|
||||
- A Proxy try to decrypt and exam HTTPS traffic for security purpose but cause the actions-runner to fail to finish SSL handshake due to the lack of trusting proxy's CA.
|
||||
|
||||
- The SSL handshake may fail if the client and server do not support the same TLS version, or the same cipher suites.
|
||||
|
||||
- A Proxy may try to modify the HTTPS request (like add or change some http headers) and causes the request become incompatible with the Actions Service (ASP.NetCore), Ex: [Nginx](https://github.com/dotnet/aspnetcore/issues/17081)
|
||||
|
||||
- Firewall rules that block action runner from accessing certain hosts, ex: `*.github.com`, `*.actions.githubusercontent.com`, etc
|
||||
|
||||
|
||||
### Identify and solve these problems
|
||||
|
||||
The key is to figure out where is the problem, the network environment, or the actions runner?
|
||||
|
||||
Use a 3rd party tool to make the same requests as the runner did would be a good start point.
|
||||
|
||||
- Use `nslookup` to check DNS
|
||||
- Use `ping` to check Ping
|
||||
- Use `traceroute`, `tracepath`, or `tracert` to check the network route between the runner and the Actions service
|
||||
- Use `curl -v` to check the network stack, good for verifying default certificate/proxy settings.
|
||||
- Use `Invoke-WebRequest` from `pwsh` (`PowerShell Core`) to check the dotnet network stack, good for verifying bugs in the dotnet framework.
|
||||
|
||||
If the 3rd party tool is also experiencing the same error as the runner does, then you might want to contact your network administrator for help.
|
||||
|
||||
Otherwise, contact GitHub customer support or log an issue at https://github.com/actions/runner
|
||||
|
||||
### Troubleshooting: Why can't I configure a runner?
|
||||
|
||||
If you are having trouble connecting, try these steps:
|
||||
|
||||
1. Validate you can reach our endpoints from your web browser. If not, double check your local network connection
|
||||
- For hosted Github:
|
||||
- https://api.github.com/
|
||||
- https://vstoken.actions.githubusercontent.com/_apis/health
|
||||
- https://pipelines.actions.githubusercontent.com/_apis/health
|
||||
- For GHES/GHAE
|
||||
- https://myGHES.com/_services/vstoken/_apis/health
|
||||
- https://myGHES.com/_services/pipelines/_apis/health
|
||||
- https://myGHES.com/api/v3
|
||||
2. Validate you can reach those endpoints in powershell core
|
||||
- The runner runs on .net core, lets validate the local settings for that stack
|
||||
- Open up `pwsh`
|
||||
- Run the command using the urls above `Invoke-WebRequest {url}`
|
||||
3. If not, get a packet trace using a tool like wireshark and start looking at the TLS handshake.
|
||||
- If you see a Client Hello followed by a Server RST:
|
||||
- You may need to configure your TLS settings to use the correct version
|
||||
- You should support TLS version 1.2 or later
|
||||
- You may need to configure your TLS settings to have up to date cipher suites, this may be solved by system updates and patches.
|
||||
- Most notably, on windows server 2012 make sure [the tls cipher suite update](https://support.microsoft.com/en-us/topic/update-adds-new-tls-cipher-suites-and-changes-cipher-suite-priorities-in-windows-8-1-and-windows-server-2012-r2-8e395e43-c8ef-27d8-b60c-0fc57d526d94) is installed
|
||||
- Your firewall, proxy or network configuration may be blocking the connection
|
||||
- You will want to reach out to whoever is in charge of your network with these pcap files to further troubleshoot
|
||||
- If you see a failure later in the handshake:
|
||||
- Try the fix in the [SSLCert Fix](./sslcert.md)
|
||||
30
docs/checks/nodejs.md
Normal file
30
docs/checks/nodejs.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Node.js Connection Check
|
||||
|
||||
## What is this check for?
|
||||
|
||||
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/`.
|
||||
|
||||
All javascript base Actions will get executed by the built-in `node` at `<runner_root>/externals/node12/`.
|
||||
|
||||
> Not the `node` from `$PATH`
|
||||
|
||||
## What is checked?
|
||||
|
||||
- Make HTTPS GET to https://api.github.com or https://myGHES.com/api/v3 using node.js, make sure it gets 200 response code.
|
||||
|
||||
## How to fix the issue?
|
||||
|
||||
### 1. Check the common network issue
|
||||
|
||||
> Please check the [network doc](./network.md)
|
||||
|
||||
### 2. SSL certificate related issue
|
||||
|
||||
If you are seeing `Https request failed due to SSL cert issue` in the log, it means the `node.js` can't connect to the GitHub server due to SSL handshake failure.
|
||||
> Please check the [SSL cert doc](./sslcert.md)
|
||||
|
||||
## 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.
|
||||
89
docs/checks/sslcert.md
Normal file
89
docs/checks/sslcert.md
Normal file
@@ -0,0 +1,89 @@
|
||||
## SSL Certificate Related Issues
|
||||
|
||||
You might run into an SSL certificate error when your GitHub Enterprise Server is using a self-signed SSL server certificate or a web proxy within your network is decrypting HTTPS traffic for a security audit.
|
||||
|
||||
As long as your certificate is generated properly, most of the issues should be fixed after your trust the certificate properly on the runner machine.
|
||||
|
||||
> Different OS might have extra requirements on SSL certificate,
|
||||
> Ex: macOS requires `ExtendedKeyUsage` https://support.apple.com/en-us/HT210176
|
||||
|
||||
### Don't skip SSL cert validation
|
||||
|
||||
> !!! DO NOT SKIP SSL CERT VALIDATION !!!
|
||||
> !!! IT IS A BAD SECURITY PRACTICE !!!
|
||||
|
||||
### Download SSL certificate chain
|
||||
|
||||
Depends on how your SSL server certificate gets configured, you might need to download the whole certificate chain from a machine that has trusted the SSL certificate's CA.
|
||||
|
||||
- Approach 1: Download certificate chain using a browser (Chrome, Firefox, IT), you can google for more example, [here is what I found](https://medium.com/@menakajain/export-download-ssl-certificate-from-server-site-url-bcfc41ea46a2)
|
||||
|
||||
- Approach 2: Download certificate chain using OpenSSL, you can google for more example, [here is what I found](https://superuser.com/a/176721)
|
||||
|
||||
- Approach 3: Ask your network administrator or the owner of the CA certificate to send you a copy of it
|
||||
|
||||
### Trust CA certificate for the Runner
|
||||
|
||||
The actions runner is a dotnet core application which will follow how dotnet load SSL CA certificates on each OS.
|
||||
|
||||
You can get full details documentation at [here](https://docs.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography#x509store)
|
||||
|
||||
In short:
|
||||
- Windows: Load from Windows certificate store.
|
||||
- Linux: Load from OpenSSL CA cert bundle.
|
||||
- macOS: Load from macOS KeyChain.
|
||||
|
||||
To let the runner trusts your CA certificate, you will need to:
|
||||
1. Save your SSL certificate chain which includes the root CA and all intermediate CAs into a `.pem` file.
|
||||
2. Use `OpenSSL` to convert `.pem` file to a proper format for different OS, here is some [doc with sample commands](https://www.sslshopper.com/ssl-converter.html)
|
||||
3. Trust CA on different OS:
|
||||
- Windows: https://docs.microsoft.com/en-us/skype-sdk/sdn/articles/installing-the-trusted-root-certificate
|
||||
- macOS: 
|
||||
- Linux: Refer to the distribution documentation
|
||||
1. RedHat: https://www.redhat.com/sysadmin/ca-certificates-cli
|
||||
2. Ubuntu: http://manpages.ubuntu.com/manpages/focal/man8/update-ca-certificates.8.html
|
||||
3. Google search: "trust ca certificate on [linux distribution]"
|
||||
4. If all approaches failed, set environment variable `SSL_CERT_FILE` to the CA bundle `.pem` file we get.
|
||||
> To verify cert gets installed properly on Linux, you can try use `curl -v https://sitewithsslissue.com` and `pwsh -Command \"Invoke-WebRequest -Uri https://sitewithsslissue.com\"`
|
||||
|
||||
### Trust CA certificate for Git CLI
|
||||
|
||||
Git uses various CA bundle file depends on your operation system.
|
||||
- Git packaged the CA bundle file within the Git installation on Windows
|
||||
- Git use OpenSSL certificate CA bundle file on Linux and macOS
|
||||
|
||||
You can check where Git check CA file by running:
|
||||
```bash
|
||||
export GIT_CURL_VERBOSE=1
|
||||
git ls-remote https://github.com/actions/runner HEAD
|
||||
```
|
||||
|
||||
You should see something like:
|
||||
```
|
||||
* Couldn't find host github.com in the .netrc file; using defaults
|
||||
* Trying 140.82.114.4...
|
||||
* TCP_NODELAY set
|
||||
* Connected to github.com (140.82.114.4) port 443 (#0)
|
||||
* ALPN, offering h2
|
||||
* ALPN, offering http/1.1
|
||||
* successfully set certificate verify locations:
|
||||
* CAfile: /etc/ssl/cert.pem
|
||||
CApath: none
|
||||
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
|
||||
```
|
||||
This tells me `/etc/ssl/cert.pem` is where it read trusted CA certificates.
|
||||
|
||||
To let Git trusts your CA certificate, you will need to:
|
||||
1. Save your SSL certificate chain which includes the root CA and all intermediate CAs into a `.pem` file.
|
||||
2. Set `http.sslCAInfo` Git config or `GIT_SSL_CAINFO` environment variable to the full path of the `.pem` file [Git Doc](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslCAInfo)
|
||||
> I would recommend using `http.sslCAInfo` since it can be scope to certain hosts that need the extra trusted CA.
|
||||
> Ex: `git config --global http.https://myghes.com/.sslCAInfo /extra/ca/cert.pem`
|
||||
> This will make Git use the `/extra/ca/cert.pem` only when communicates with `https://myghes.com` and keep using the default CA bundle with others.
|
||||
|
||||
### Trust CA certificate for Node.js
|
||||
|
||||
Node.js has compiled a snapshot of the Mozilla CA store that is fixed at each version of Node.js' release time.
|
||||
|
||||
To let Node.js trusts your CA certificate, you will need to:
|
||||
1. Save your SSL certificate chain which includes the root CA and all intermediate CAs into a `.pem` file.
|
||||
2. Set environment variable `NODE_EXTRA_CA_CERTS` which point to the file. ex: `export NODE_EXTRA_CA_CERTS=/full/path/to/cacert.pem` or `set NODE_EXTRA_CA_CERTS=C:\full\path\to\cacert.pem`
|
||||
@@ -12,20 +12,43 @@ Issues in this repository should be for the runner application. Note that the V
|
||||
|
||||
## Enhancements and Feature Requests
|
||||
|
||||
We ask that before significant effort is put into code changes, that we have agreement on taking the change before time is invested in code changes.
|
||||
We ask that before significant effort is put into code changes, that we have agreement on taking the change before time is invested in code changes.
|
||||
|
||||
1. Create a feature request. Once agreed we will take the enhancment
|
||||
1. Create a feature request. Once agreed we will take the enhancement
|
||||
2. Create an ADR to agree on the details of the change.
|
||||
|
||||
An ADR is an Architectural Decision Record. This allows consensus on the direction forward and also serves as a record of the change and motivation. [Read more here](adrs/README.md)
|
||||
|
||||
## Required Dev Dependencies
|
||||
|
||||
  Git for Windows and Linux [Install Here](https://git-scm.com/downloads) (needed for dev sh script)
|
||||
|
||||
## 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:
|
||||
|
||||
- The url of your repository
|
||||
- A runner registration token. You can find it at `https://github.com/{your-repo}/settings/actions/runners/new`
|
||||
|
||||
|
||||
```bash
|
||||
git clone https://github.com/actions/runner
|
||||
cd runner/src
|
||||
./dev.(sh/cmd) layout # the runner that built from source is in {root}/_layout
|
||||
cd ../_layout
|
||||
./config.(sh/cmd) --url https://github.com/{your-repo} --token ABCABCABCABCABCABCABCABCABCAB # accept default name, labels and work folder
|
||||
./run.(sh/cmd)
|
||||
```
|
||||
|
||||
If you trigger a job now, you can see the runner execute it.
|
||||
|
||||
Tip: Make sure your job can run on this runner. The easiest way is to set `runs-on: self-hosted` in the workflow file.
|
||||
|
||||
|
||||
## Development Life Cycle
|
||||
If you're using VS Code, you can follow [these](contribute/vscode.md) steps instead.
|
||||
|
||||
### Required Dev Dependencies
|
||||
|
||||
 Git for Windows [Install Here](https://git-scm.com/downloads) (needed for dev sh script)
|
||||
|
||||
### To Build, Test, Layout
|
||||
### To Build, Test, Layout
|
||||
|
||||
Navigate to the `src` directory and run the following command:
|
||||
|
||||
@@ -39,10 +62,11 @@ Navigate to the `src` directory and run the following command:
|
||||
* `build` (`b`): Build everything and update runner layout folder
|
||||
* `test` (`t`): Build runner binaries and run unit tests
|
||||
|
||||
Sample developer flow:
|
||||
### Sample developer flow:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/actions/runner
|
||||
cd runner
|
||||
cd ./src
|
||||
./dev.(sh/cmd) layout # the runner that built from source is in {root}/_layout
|
||||
<make code changes>
|
||||
@@ -50,12 +74,81 @@ cd ./src
|
||||
./dev.(sh/cmd) test # run all unit tests before git commit/push
|
||||
```
|
||||
|
||||
### Editors
|
||||
Let's break that down.
|
||||
|
||||
### Clone repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/actions/runner
|
||||
cd runner
|
||||
```
|
||||
If you want to push your changes to a remote, it is recommended you fork the repository and use that fork as your origin instead of `https://github.com/actions/runner`.
|
||||
|
||||
|
||||
### Build Layout:
|
||||
|
||||
This command will build all projects, then copies them and other dependencies into a folder called `_layout`. The binaries in this folder are then used for running, debugging the runner.
|
||||
|
||||
```bash
|
||||
cd ./src # execute the script from this folder
|
||||
./dev.(sh/cmd) layout # the runner that built from source is in {root}/_layout
|
||||
```
|
||||
|
||||
If you make code changes after this point, use the argument `build` to build your code in the `src` folder to keep your `_layout` folder up to date.
|
||||
|
||||
```bash
|
||||
cd ./src
|
||||
./dev.(sh/cmd) build # {root}/_layout will get updated
|
||||
```
|
||||
### Test Layout:
|
||||
|
||||
This command runs the suite of unit tests in the project
|
||||
|
||||
```bash
|
||||
cd ./src
|
||||
./dev.(sh/cmd) test # run all unit tests before git commit/push
|
||||
```
|
||||
|
||||
### Configure Runner:
|
||||
|
||||
If you want to manually test your runner and run actions from a real repository, you'll have to configure it before running it.
|
||||
|
||||
```bash
|
||||
cd runner/_layout
|
||||
./config.(sh/cmd) # configure your custom runner
|
||||
```
|
||||
|
||||
You will need your the name of your repository and a runner registration token.
|
||||
Check [Quickstart](##Quickstart:-Run-a-job-from-a-real-repository) if you don't know how to get this token.
|
||||
|
||||
These can also be passed down as arguments to `config.(sh/cmd)`:
|
||||
```bash
|
||||
cd runner/_layout
|
||||
./config.(sh/cmd) --url https://github.com/{your-repo} --token ABCABCABCABCABCABCABCABCABCAB
|
||||
```
|
||||
|
||||
### Run Runner
|
||||
|
||||
All that's left to do is to start the runner:
|
||||
```bash
|
||||
cd runner/_layout
|
||||
./run.(sh/cmd) # run your custom runner
|
||||
```
|
||||
|
||||
### View logs:
|
||||
|
||||
```bash
|
||||
cd runner/_layout/_diag
|
||||
ls
|
||||
cat (Runner/Worker)_TIMESTAMP.log # view your log file
|
||||
```
|
||||
|
||||
## Editors
|
||||
|
||||
[Using Visual Studio Code](https://code.visualstudio.com/)
|
||||
[Using Visual Studio 2019](https://www.visualstudio.com/vs/)
|
||||
[Using Visual Studio](https://code.visualstudio.com/docs)
|
||||
|
||||
### Styling
|
||||
## Styling
|
||||
|
||||
We use the .NET Foundation and CoreCLR style guidelines [located here](
|
||||
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md)
|
||||
|
||||
52
docs/contribute/vscode.md
Normal file
52
docs/contribute/vscode.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Development Life Cycle using VS Code:
|
||||
|
||||
These examples use VS Code, but the idea should be similar across all IDEs as long as you attach to the same processes in the right folder.
|
||||
## Configure
|
||||
|
||||
To successfully start the runner, you need to register it using a repository and a runner registration token.
|
||||
Run `Configure` first to build the source code and set up the runner in `_layout`.
|
||||
Once it's done creating `_layout`, it asks for the url of your repository and your token in the terminal.
|
||||
|
||||
Check [Quickstart](../contribute.md#quickstart-run-a-job-from-a-real-repository) if you don't know how to get this token.
|
||||
|
||||
## Debugging
|
||||
|
||||
Debugging the full lifecycle of a job can be tricky, because there are multiple processes involved.
|
||||
All the configs below can be found in `.vscode/launch.json`.
|
||||
|
||||
## Debug the Listener
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Run [build]",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build runner layout", // use the config called "Run" to launch without rebuild
|
||||
"program": "${workspaceFolder}/_layout/bin/Runner.Listener",
|
||||
"args": [
|
||||
"run" // run without args to print usage
|
||||
],
|
||||
"cwd": "${workspaceFolder}/src",
|
||||
"console": "integratedTerminal",
|
||||
"requireExactSource": false,
|
||||
}
|
||||
```
|
||||
|
||||
If you launch `Run` or `Run [build]`, it starts a process called `Runner.Listener`.
|
||||
This process will receive any job queued on this repository if the job runs on matching labels (e.g `runs-on: self-hosted`).
|
||||
Once a job is received, a `Runner.Listener` starts a new process of `Runner.Worker`.
|
||||
Since this is a diferent process, you can't use the same debugger session debug it.
|
||||
Instead, a parallel debugging session has to be started, using a different launch config.
|
||||
Luckily, VS Code supports multiple parallel debugging sessions.
|
||||
|
||||
## Debug the Worker
|
||||
|
||||
Because the worker process is usually started by the listener instead of an IDE, debugging it from start to finish can be tricky.
|
||||
For this reason, `Runner.Worker` can be configured to wait for a debugger to be attached before it begins any actual work.
|
||||
|
||||
Set the environment variable `GITHUB_ACTIONS_RUNNER_ATTACH_DEBUGGER` to `true` or `1` to enable this wait.
|
||||
All worker processes now will wait 20 seconds before they start working on their task.
|
||||
|
||||
This gives enough time to attach a debugger by running `Debug Worker`.
|
||||
If for some reason you have multiple workers running, run the launch config `Attach` instead.
|
||||
Select `Runner.Worker` from the running processes when VS Code prompts for it.
|
||||
@@ -58,4 +58,4 @@ Authentication in a workflow run to github.com can be accomplished by using the
|
||||
|
||||
Hosted runner authentication differs from self-hosted authentication in that runners do not undergo a registration process, but instead, the hosted runners get the OAuth token directly by reading the `.credentials` file. The scope of this particular token is limited for a given workflow job execution, and the token is revoked as soon as the job is finished.
|
||||
|
||||

|
||||

|
||||
|
||||
BIN
docs/res/macOStrustCA.gif
Normal file
BIN
docs/res/macOStrustCA.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 MiB |
@@ -27,7 +27,7 @@ Dependencies is missing for Dotnet Core 3.0
|
||||
Execute ./bin/installdependencies.sh to install any missing Dotnet Core 3.0 dependencies.
|
||||
```
|
||||
You can easily correct the problem by executing `./bin/installdependencies.sh`.
|
||||
The `installdependencies.sh` script should install all required dependencies on all supported Linux versions
|
||||
The `installdependencies.sh` script should install all required dependencies on all supported Linux versions
|
||||
> Note: The `installdependencies.sh` script will try to use the default package management mechanism on your Linux flavor (ex. `yum`/`apt-get`/`apt`).
|
||||
|
||||
### Full dependencies list
|
||||
@@ -35,15 +35,15 @@ The `installdependencies.sh` script should install all required dependencies on
|
||||
Debian based OS (Debian, Ubuntu, Linux Mint)
|
||||
|
||||
- liblttng-ust0
|
||||
- libkrb5-3
|
||||
- libkrb5-3
|
||||
- zlib1g
|
||||
- libssl1.1, libssl1.0.2 or libssl1.0.0
|
||||
- libicu63, libicu60, libicu57 or libicu55
|
||||
|
||||
Fedora based OS (Fedora, Redhat, Centos, Oracle Linux 7)
|
||||
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
|
||||
|
||||
- lttng-ust
|
||||
- openssl-libs
|
||||
- lttng-ust
|
||||
- openssl-libs
|
||||
- krb5-libs
|
||||
- zlib
|
||||
- libicu
|
||||
|
||||
@@ -6,5 +6,11 @@
|
||||
|
||||
- macOS High Sierra (10.13) and later versions
|
||||
|
||||
## Apple Silicon M1
|
||||
|
||||
The runner is currently not supported on devices with an Apple M1 chip.
|
||||
We are waiting for official .NET support. You can read more here about the [current state of support here](https://github.com/orgs/dotnet/projects/18#card-56812463).
|
||||
Current .NET project board about M1 support:
|
||||
https://github.com/orgs/dotnet/projects/18#card-56812463
|
||||
|
||||
## [More .Net Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/macos-prerequisites?tabs=netcore30)
|
||||
|
||||
@@ -1,80 +1,87 @@
|
||||
## Features
|
||||
- Update Runner Register GitHub API URL to Support Org-level Runner (#339 #345 #352)
|
||||
- Preserve workflow file/line/column for better error messages (#356)
|
||||
- Switch to use token service instead of SPS for exchanging oauth token. (#325)
|
||||
- Load and print machine setup info from .setup_info (#364)
|
||||
- Expose job name as $GITHUB_JOB (#366)
|
||||
- Add support for job outputs. (#365)
|
||||
- Set CI=true when launch process in actions runner. (#374)
|
||||
- Set steps.<id>.outcome and steps.<id>.conclusion. (#372)
|
||||
- Add support for workflow/job defaults. (#369)
|
||||
- Expose GITHUB_REPOSITORY_OWNER and ${{github.repository_owner}}. (#378)
|
||||
|
||||
- Expose GITHUB_REF_* as environment variable (#1314)
|
||||
- Add arch to runner context (#1372)
|
||||
- Support Conditional Steps in Composite Actions (#1438)
|
||||
- Log current runner version in terminal (#1441)
|
||||
|
||||
## Bugs
|
||||
- Use authenticate endpoint for testing runner connection. (#311)
|
||||
- Commands translate file path from container action (#331)
|
||||
- Change problem matchers output to debug (#363)
|
||||
- Switch hashFiles to extension function (#362)
|
||||
- Add expanded volumes strings to container mounts (#384)
|
||||
|
||||
- Makes the user keychains available to the service (#847)
|
||||
- Use Actions Service health and api.github.com endpoints after connection failure on Actions Server and Hosted (#1385)
|
||||
- Fix an issue where nested local composite actions did not correctly register post steps (#1433)
|
||||
|
||||
## Misc
|
||||
- Add runner auth documentation (#357)
|
||||
|
||||
- Cleanup Older versions on MacOS now that we recreate node versions as needed (#1410)
|
||||
|
||||
## 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
|
||||
```
|
||||
// Create a folder under the drive root
|
||||
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.
|
||||
|
||||
The following snipped needs to be run on `powershell`:
|
||||
``` powershell
|
||||
# Create a folder under the drive root
|
||||
mkdir \actions-runner ; cd \actions-runner
|
||||
// Download the latest runner package
|
||||
# Download the latest runner package
|
||||
Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-win-x64-<RUNNER_VERSION>.zip -OutFile actions-runner-win-x64-<RUNNER_VERSION>.zip
|
||||
// Extract the installer
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
||||
# Extract the installer
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD\actions-runner-win-x64-<RUNNER_VERSION>.zip", "$PWD")
|
||||
```
|
||||
|
||||
## OSX
|
||||
|
||||
``` bash
|
||||
// Create a folder
|
||||
# Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
# Download the latest runner package
|
||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
# Extract the installer
|
||||
tar xzf ./actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
|
||||
## Linux x64
|
||||
|
||||
``` bash
|
||||
// Create a folder
|
||||
# Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
# Download the latest runner package
|
||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
# Extract the installer
|
||||
tar xzf ./actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
|
||||
## Linux arm64 (Pre-release)
|
||||
## Linux arm64
|
||||
|
||||
``` bash
|
||||
// Create a folder
|
||||
# Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
# Download the latest runner package
|
||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
# Extract the installer
|
||||
tar xzf ./actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
|
||||
## Linux arm (Pre-release)
|
||||
## Linux arm
|
||||
|
||||
``` bash
|
||||
// Create a folder
|
||||
# Create a folder
|
||||
mkdir actions-runner && cd actions-runner
|
||||
// Download the latest runner package
|
||||
# Download the latest runner package
|
||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||
// Extract the installer
|
||||
# Extract the installer
|
||||
tar xzf ./actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||
```
|
||||
|
||||
## Using your self hosted runner
|
||||
For additional details about configuring, running, or shutting down the runner please check out our [product docs.](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/adding-self-hosted-runners)
|
||||
|
||||
## SHA-256 Checksums
|
||||
|
||||
The SHA-256 checksums for the packages included in this build are shown below:
|
||||
|
||||
- actions-runner-win-x64-<RUNNER_VERSION>.zip <!-- BEGIN SHA win-x64 --><WIN_X64_SHA><!-- END SHA win-x64 -->
|
||||
- actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz <!-- BEGIN SHA osx-x64 --><OSX_X64_SHA><!-- END SHA osx-x64 -->
|
||||
- actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz <!-- BEGIN SHA linux-x64 --><LINUX_X64_SHA><!-- END SHA linux-x64 -->
|
||||
- actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz <!-- BEGIN SHA linux-arm64 --><LINUX_ARM64_SHA><!-- END SHA linux-arm64 -->
|
||||
- actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz <!-- BEGIN SHA linux-arm --><LINUX_ARM_SHA><!-- END SHA linux-arm -->
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.164.0
|
||||
<Update to ./src/runnerversion when creating release>
|
||||
|
||||
4
scripts/README.md
Normal file
4
scripts/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Sample scripts for self-hosted runners
|
||||
|
||||
Here are some examples to work from if you'd like to automate your use of self-hosted runners.
|
||||
See the docs [here](../docs/automate.md).
|
||||
181
scripts/create-latest-svc.sh
Executable file
181
scripts/create-latest-svc.sh
Executable file
@@ -0,0 +1,181 @@
|
||||
#/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Notes:
|
||||
# PATS over envvars are more secure
|
||||
# Downloads latest runner release (not pre-release)
|
||||
# Configures it as a service more secure
|
||||
# Should be used on VMs and not containers
|
||||
# Works on OSX and Linux
|
||||
# Assumes x64 arch
|
||||
# See EXAMPLES below
|
||||
|
||||
flags_found=false
|
||||
|
||||
while getopts 's:g:n:u:l:' opt; do
|
||||
flags_found=true
|
||||
|
||||
case $opt in
|
||||
s)
|
||||
runner_scope=$OPTARG
|
||||
;;
|
||||
g)
|
||||
ghe_hostname=$OPTARG
|
||||
;;
|
||||
n)
|
||||
runner_name=$OPTARG
|
||||
;;
|
||||
u)
|
||||
svc_user=$OPTARG
|
||||
;;
|
||||
l)
|
||||
labels=$OPTARG
|
||||
;;
|
||||
*)
|
||||
echo "
|
||||
Runner Service Installer
|
||||
Examples:
|
||||
RUNNER_CFG_PAT=<yourPAT> ./create-latest-svc.sh myuser/myrepo my.ghe.deployment.net
|
||||
RUNNER_CFG_PAT=<yourPAT> ./create-latest-svc.sh -s myorg -u user_name -l label1,label2
|
||||
Usage:
|
||||
export RUNNER_CFG_PAT=<yourPAT>
|
||||
./create-latest-svc scope [ghe_domain] [name] [user] [labels]
|
||||
-s required scope: repo (:owner/:repo) or org (:organization)
|
||||
-g optional ghe_hostname: the fully qualified domain name of your GitHub Enterprise Server deployment
|
||||
-n optional name of the runner, defaults to hostname
|
||||
-u optional user svc will run as, defaults to current
|
||||
-l optional list of labels (split by comma) applied on the runner"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift "$((OPTIND - 1))"
|
||||
|
||||
if ! "$flags_found"; then
|
||||
runner_scope=${1}
|
||||
ghe_hostname=${2}
|
||||
runner_name=${3:-$(hostname)}
|
||||
svc_user=${4:-$USER}
|
||||
labels=${5}
|
||||
fi
|
||||
|
||||
# apply defaults
|
||||
runner_name=${runner_name:-$(hostname)}
|
||||
svc_user=${svc_user:-$USER}
|
||||
|
||||
echo "Configuring runner @ ${runner_scope}"
|
||||
sudo echo
|
||||
|
||||
#---------------------------------------
|
||||
# Validate Environment
|
||||
#---------------------------------------
|
||||
runner_plat=linux
|
||||
[ ! -z "$(which sw_vers)" ] && runner_plat=osx;
|
||||
|
||||
function fatal()
|
||||
{
|
||||
echo "error: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi
|
||||
if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi
|
||||
|
||||
which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc"
|
||||
which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc"
|
||||
|
||||
# bail early if there's already a runner there. also sudo early
|
||||
if [ -d ./runner ]; then
|
||||
fatal "Runner already exists. Use a different directory or delete ./runner"
|
||||
fi
|
||||
|
||||
sudo -u ${svc_user} mkdir runner
|
||||
|
||||
# TODO: validate not in a container
|
||||
# TODO: validate systemd or osx svc installer
|
||||
|
||||
#--------------------------------------
|
||||
# Get a config token
|
||||
#--------------------------------------
|
||||
echo
|
||||
echo "Generating a registration token..."
|
||||
|
||||
base_api_url="https://api.github.com"
|
||||
if [ -n "${ghe_hostname}" ]; then
|
||||
base_api_url="https://${ghe_hostname}/api/v3"
|
||||
fi
|
||||
|
||||
# if the scope has a slash, it's a repo runner
|
||||
orgs_or_repos="orgs"
|
||||
if [[ "$runner_scope" == *\/* ]]; then
|
||||
orgs_or_repos="repos"
|
||||
fi
|
||||
|
||||
export RUNNER_TOKEN=$(curl -s -X POST ${base_api_url}/${orgs_or_repos}/${runner_scope}/actions/runners/registration-token -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" | jq -r '.token')
|
||||
|
||||
if [ "null" == "$RUNNER_TOKEN" -o -z "$RUNNER_TOKEN" ]; then fatal "Failed to get a token"; fi
|
||||
|
||||
#---------------------------------------
|
||||
# Download latest released and extract
|
||||
#---------------------------------------
|
||||
echo
|
||||
echo "Downloading latest runner ..."
|
||||
|
||||
# For the GHES Alpha, download the runner from github.com
|
||||
latest_version_label=$(curl -s -X GET 'https://api.github.com/repos/actions/runner/releases/latest' | jq -r '.tag_name')
|
||||
latest_version=$(echo ${latest_version_label:1})
|
||||
runner_file="actions-runner-${runner_plat}-x64-${latest_version}.tar.gz"
|
||||
|
||||
if [ -f "${runner_file}" ]; then
|
||||
echo "${runner_file} exists. skipping download."
|
||||
else
|
||||
runner_url="https://github.com/actions/runner/releases/download/${latest_version_label}/${runner_file}"
|
||||
|
||||
echo "Downloading ${latest_version_label} for ${runner_plat} ..."
|
||||
echo $runner_url
|
||||
|
||||
curl -O -L ${runner_url}
|
||||
fi
|
||||
|
||||
ls -la *.tar.gz
|
||||
|
||||
#---------------------------------------------------
|
||||
# extract to runner directory in this directory
|
||||
#---------------------------------------------------
|
||||
echo
|
||||
echo "Extracting ${runner_file} to ./runner"
|
||||
|
||||
tar xzf "./${runner_file}" -C runner
|
||||
|
||||
# export of pass
|
||||
sudo chown -R $svc_user ./runner
|
||||
|
||||
pushd ./runner
|
||||
|
||||
#---------------------------------------
|
||||
# Unattend config
|
||||
#---------------------------------------
|
||||
runner_url="https://github.com/${runner_scope}"
|
||||
if [ -n "${ghe_hostname}" ]; then
|
||||
runner_url="https://${ghe_hostname}/${runner_scope}"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Configuring ${runner_name} @ $runner_url"
|
||||
echo "./config.sh --unattended --url $runner_url --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
|
||||
|
||||
#---------------------------------------
|
||||
# Configuring as a service
|
||||
#---------------------------------------
|
||||
echo
|
||||
echo "Configuring as a service ..."
|
||||
prefix=""
|
||||
if [ "${runner_plat}" == "linux" ]; then
|
||||
prefix="sudo "
|
||||
fi
|
||||
|
||||
${prefix}./svc.sh install ${svc_user}
|
||||
${prefix}./svc.sh start
|
||||
83
scripts/delete.sh
Executable file
83
scripts/delete.sh
Executable file
@@ -0,0 +1,83 @@
|
||||
#/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
#
|
||||
# Force deletes a runner from the service
|
||||
# The caller should have already ensured the runner is gone and/or stopped
|
||||
#
|
||||
# Examples:
|
||||
# RUNNER_CFG_PAT=<yourPAT> ./delete.sh myuser/myrepo myname
|
||||
# RUNNER_CFG_PAT=<yourPAT> ./delete.sh myorg
|
||||
#
|
||||
# Usage:
|
||||
# export RUNNER_CFG_PAT=<yourPAT>
|
||||
# ./delete.sh scope name
|
||||
#
|
||||
# scope required repo (:owner/:repo) or org (:organization)
|
||||
# name optional defaults to hostname. name to delete
|
||||
#
|
||||
# Notes:
|
||||
# PATS over envvars are more secure
|
||||
# Works on OSX and Linux
|
||||
# Assumes x64 arch
|
||||
#
|
||||
|
||||
runner_scope=${1}
|
||||
runner_name=${2}
|
||||
|
||||
echo "Deleting runner ${runner_name} @ ${runner_scope}"
|
||||
|
||||
function fatal()
|
||||
{
|
||||
echo "error: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi
|
||||
if [ -z "${runner_name}" ]; then fatal "supply name as argument 2"; fi
|
||||
if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi
|
||||
|
||||
which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc"
|
||||
which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc"
|
||||
|
||||
base_api_url="https://api.github.com/orgs"
|
||||
if [[ "$runner_scope" == *\/* ]]; then
|
||||
base_api_url="https://api.github.com/repos"
|
||||
fi
|
||||
|
||||
|
||||
#--------------------------------------
|
||||
# Ensure offline
|
||||
#--------------------------------------
|
||||
runner_status=$(curl -s -X GET ${base_api_url}/${runner_scope}/actions/runners?per_page=100 -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" \
|
||||
| jq -M -j ".runners | .[] | select(.name == \"${runner_name}\") | .status")
|
||||
|
||||
if [ -z "${runner_status}" ]; then
|
||||
fatal "Could not find runner with name ${runner_name}"
|
||||
fi
|
||||
|
||||
echo "Status: ${runner_status}"
|
||||
|
||||
if [ "${runner_status}" != "offline" ]; then
|
||||
fatal "Runner should be offline before removing"
|
||||
fi
|
||||
|
||||
#--------------------------------------
|
||||
# Get id of runner to remove
|
||||
#--------------------------------------
|
||||
runner_id=$(curl -s -X GET ${base_api_url}/${runner_scope}/actions/runners?per_page=100 -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" \
|
||||
| jq -M -j ".runners | .[] | select(.name == \"${runner_name}\") | .id")
|
||||
|
||||
if [ -z "${runner_id}" ]; then
|
||||
fatal "Could not find runner with name ${runner_name}"
|
||||
fi
|
||||
|
||||
echo "Removing id ${runner_id}"
|
||||
|
||||
#--------------------------------------
|
||||
# Remove the runner
|
||||
#--------------------------------------
|
||||
curl -s -X DELETE ${base_api_url}/${runner_scope}/actions/runners/${runner_id} -H "authorization: token ${RUNNER_CFG_PAT}"
|
||||
|
||||
echo "Done."
|
||||
76
scripts/remove-svc.sh
Executable file
76
scripts/remove-svc.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
#
|
||||
# Removes a runner running as a service
|
||||
# Must be run on the machine where the service is run
|
||||
#
|
||||
# Examples:
|
||||
# RUNNER_CFG_PAT=<yourPAT> ./remove-svc.sh myuser/myrepo
|
||||
# RUNNER_CFG_PAT=<yourPAT> ./remove-svc.sh myorg
|
||||
#
|
||||
# Usage:
|
||||
# export RUNNER_CFG_PAT=<yourPAT>
|
||||
# ./remove-svc scope name
|
||||
#
|
||||
# scope required repo (:owner/:repo) or org (:organization)
|
||||
# name optional defaults to hostname. name to uninstall and remove
|
||||
#
|
||||
# Notes:
|
||||
# PATS over envvars are more secure
|
||||
# Should be used on VMs and not containers
|
||||
# Works on OSX and Linux
|
||||
# Assumes x64 arch
|
||||
#
|
||||
|
||||
runner_scope=${1}
|
||||
runner_name=${2:-$(hostname)}
|
||||
|
||||
echo "Uninstalling runner ${runner_name} @ ${runner_scope}"
|
||||
sudo echo
|
||||
|
||||
function fatal()
|
||||
{
|
||||
echo "error: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi
|
||||
if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi
|
||||
|
||||
which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc"
|
||||
which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc"
|
||||
|
||||
runner_plat=linux
|
||||
[ ! -z "$(which sw_vers)" ] && runner_plat=osx;
|
||||
|
||||
#--------------------------------------
|
||||
# Get a remove token
|
||||
#--------------------------------------
|
||||
echo
|
||||
echo "Generating a removal token..."
|
||||
|
||||
# if the scope has a slash, it's an repo runner
|
||||
base_api_url="https://api.github.com/orgs"
|
||||
if [[ "$runner_scope" == *\/* ]]; then
|
||||
base_api_url="https://api.github.com/repos"
|
||||
fi
|
||||
|
||||
export REMOVE_TOKEN=$(curl -s -X POST ${base_api_url}/${runner_scope}/actions/runners/remove-token -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" | jq -r '.token')
|
||||
|
||||
if [ -z "$REMOVE_TOKEN" ]; then fatal "Failed to get a token"; fi
|
||||
|
||||
#---------------------------------------
|
||||
# Stop and uninstall the service
|
||||
#---------------------------------------
|
||||
echo
|
||||
echo "Uninstall the service ..."
|
||||
pushd ./runner
|
||||
prefix=""
|
||||
if [ "${runner_plat}" == "linux" ]; then
|
||||
prefix="sudo "
|
||||
fi
|
||||
${prefix}./svc.sh stop
|
||||
${prefix}./svc.sh uninstall
|
||||
./config.sh remove --token $REMOVE_TOKEN
|
||||
10
src/.editorconfig
Normal file
10
src/.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
[*.cs]
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_open_brace = all
|
||||
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29411.138
|
||||
@@ -21,6 +21,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sdk", "Sdk\Sdk.csproj", "{D
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{C932061F-F6A1-4F1E-B854-A6C6B30DC3EF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EFB254FC-7927-445E-BA64-6676ADB309E9}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
||||
519
src/Misc/dotnet-install.ps1
vendored
519
src/Misc/dotnet-install.ps1
vendored
@@ -23,8 +23,6 @@
|
||||
Default: latest
|
||||
Represents a build version on specific channel. Possible values:
|
||||
- latest - most latest build on specific channel
|
||||
- coherent - most latest coherent build on specific channel
|
||||
coherent applies only to SDK downloads
|
||||
- 3-part version in a format A.B.C - represents specific version of build
|
||||
examples: 2.0.0-preview2-006120, 1.1.0
|
||||
.PARAMETER InstallDir
|
||||
@@ -69,6 +67,8 @@
|
||||
.PARAMETER ProxyUseDefaultCredentials
|
||||
Default: false
|
||||
Use default credentials, when using proxy address.
|
||||
.PARAMETER ProxyBypassList
|
||||
If set with ProxyAddress, will provide the list of comma separated urls that will bypass the proxy
|
||||
.PARAMETER SkipNonVersionedFiles
|
||||
Default: false
|
||||
Skips installing non-versioned files if they already exist, such as dotnet.exe.
|
||||
@@ -96,6 +96,7 @@ param(
|
||||
[string]$FeedCredential,
|
||||
[string]$ProxyAddress,
|
||||
[switch]$ProxyUseDefaultCredentials,
|
||||
[string[]]$ProxyBypassList=@(),
|
||||
[switch]$SkipNonVersionedFiles,
|
||||
[switch]$NoCdn
|
||||
)
|
||||
@@ -119,11 +120,45 @@ $VersionRegEx="/\d+\.\d+[^/]+/"
|
||||
$OverrideNonVersionedFiles = !$SkipNonVersionedFiles
|
||||
|
||||
function Say($str) {
|
||||
Write-Host "dotnet-install: $str"
|
||||
try {
|
||||
Write-Host "dotnet-install: $str"
|
||||
}
|
||||
catch {
|
||||
# Some platforms cannot utilize Write-Host (Azure Functions, for instance). Fall back to Write-Output
|
||||
Write-Output "dotnet-install: $str"
|
||||
}
|
||||
}
|
||||
|
||||
function Say-Warning($str) {
|
||||
try {
|
||||
Write-Warning "dotnet-install: $str"
|
||||
}
|
||||
catch {
|
||||
# Some platforms cannot utilize Write-Warning (Azure Functions, for instance). Fall back to Write-Output
|
||||
Write-Output "dotnet-install: Warning: $str"
|
||||
}
|
||||
}
|
||||
|
||||
# Writes a line with error style settings.
|
||||
# Use this function to show a human-readable comment along with an exception.
|
||||
function Say-Error($str) {
|
||||
try {
|
||||
# Write-Error is quite oververbose for the purpose of the function, let's write one line with error style settings.
|
||||
$Host.UI.WriteErrorLine("dotnet-install: $str")
|
||||
}
|
||||
catch {
|
||||
Write-Output "dotnet-install: Error: $str"
|
||||
}
|
||||
}
|
||||
|
||||
function Say-Verbose($str) {
|
||||
Write-Verbose "dotnet-install: $str"
|
||||
try {
|
||||
Write-Verbose "dotnet-install: $str"
|
||||
}
|
||||
catch {
|
||||
# Some platforms cannot utilize Write-Verbose (Azure Functions, for instance). Fall back to Write-Output
|
||||
Write-Output "dotnet-install: $str"
|
||||
}
|
||||
}
|
||||
|
||||
function Say-Invocation($Invocation) {
|
||||
@@ -137,7 +172,7 @@ function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [int]$MaxAttempts = 3, [in
|
||||
|
||||
while ($true) {
|
||||
try {
|
||||
return $ScriptBlock.Invoke()
|
||||
return & $ScriptBlock
|
||||
}
|
||||
catch {
|
||||
$Attempts++
|
||||
@@ -154,7 +189,16 @@ function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [int]$MaxAttempts = 3, [in
|
||||
function Get-Machine-Architecture() {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
# possible values: amd64, x64, x86, arm64, arm
|
||||
# On PS x86, PROCESSOR_ARCHITECTURE reports x86 even on x64 systems.
|
||||
# To get the correct architecture, we need to use PROCESSOR_ARCHITEW6432.
|
||||
# PS x64 doesn't define this, so we fall back to PROCESSOR_ARCHITECTURE.
|
||||
# Possible values: amd64, x64, x86, arm64, arm
|
||||
|
||||
if( $ENV:PROCESSOR_ARCHITEW6432 -ne $null )
|
||||
{
|
||||
return $ENV:PROCESSOR_ARCHITEW6432
|
||||
}
|
||||
|
||||
return $ENV:PROCESSOR_ARCHITECTURE
|
||||
}
|
||||
|
||||
@@ -167,7 +211,7 @@ function Get-CLIArchitecture-From-Architecture([string]$Architecture) {
|
||||
{ $_ -eq "x86" } { return "x86" }
|
||||
{ $_ -eq "arm" } { return "arm" }
|
||||
{ $_ -eq "arm64" } { return "arm64" }
|
||||
default { throw "Architecture not supported. If you think this is a bug, report it at https://github.com/dotnet/sdk/issues" }
|
||||
default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +272,11 @@ function GetHTTPResponse([Uri] $Uri)
|
||||
|
||||
if($ProxyAddress) {
|
||||
$HttpClientHandler = New-Object System.Net.Http.HttpClientHandler
|
||||
$HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{Address=$ProxyAddress;UseDefaultCredentials=$ProxyUseDefaultCredentials}
|
||||
$HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{
|
||||
Address=$ProxyAddress;
|
||||
UseDefaultCredentials=$ProxyUseDefaultCredentials;
|
||||
BypassList = $ProxyBypassList;
|
||||
}
|
||||
$HttpClient = New-Object System.Net.Http.HttpClient -ArgumentList $HttpClientHandler
|
||||
}
|
||||
else {
|
||||
@@ -238,18 +286,41 @@ function GetHTTPResponse([Uri] $Uri)
|
||||
# Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out
|
||||
# 20 minutes allows it to work over much slower connections.
|
||||
$HttpClient.Timeout = New-TimeSpan -Minutes 20
|
||||
$Response = $HttpClient.GetAsync("${Uri}${FeedCredential}").Result
|
||||
if (($Response -eq $null) -or (-not ($Response.IsSuccessStatusCode))) {
|
||||
# The feed credential is potentially sensitive info. Do not log FeedCredential to console output.
|
||||
$ErrorMsg = "Failed to download $Uri."
|
||||
if ($Response -ne $null) {
|
||||
$ErrorMsg += " $Response"
|
||||
$Task = $HttpClient.GetAsync("${Uri}${FeedCredential}").ConfigureAwait("false");
|
||||
$Response = $Task.GetAwaiter().GetResult();
|
||||
|
||||
if (($null -eq $Response) -or (-not ($Response.IsSuccessStatusCode))) {
|
||||
# The feed credential is potentially sensitive info. Do not log FeedCredential to console output.
|
||||
$DownloadException = [System.Exception] "Unable to download $Uri."
|
||||
|
||||
if ($null -ne $Response) {
|
||||
$DownloadException.Data["StatusCode"] = [int] $Response.StatusCode
|
||||
$DownloadException.Data["ErrorMessage"] = "Unable to download $Uri. Returned HTTP status code: " + $DownloadException.Data["StatusCode"]
|
||||
}
|
||||
|
||||
throw $ErrorMsg
|
||||
throw $DownloadException
|
||||
}
|
||||
|
||||
return $Response
|
||||
return $Response
|
||||
}
|
||||
catch [System.Net.Http.HttpRequestException] {
|
||||
$DownloadException = [System.Exception] "Unable to download $Uri."
|
||||
|
||||
# Pick up the exception message and inner exceptions' messages if they exist
|
||||
$CurrentException = $PSItem.Exception
|
||||
$ErrorMsg = $CurrentException.Message + "`r`n"
|
||||
while ($CurrentException.InnerException) {
|
||||
$CurrentException = $CurrentException.InnerException
|
||||
$ErrorMsg += $CurrentException.Message + "`r`n"
|
||||
}
|
||||
|
||||
# Check if there is an issue concerning TLS.
|
||||
if ($ErrorMsg -like "*SSL/TLS*") {
|
||||
$ErrorMsg += "Ensure that TLS 1.2 or higher is enabled to use this script.`r`n"
|
||||
}
|
||||
|
||||
$DownloadException.Data["ErrorMessage"] = $ErrorMsg
|
||||
throw $DownloadException
|
||||
}
|
||||
finally {
|
||||
if ($HttpClient -ne $null) {
|
||||
@@ -259,7 +330,7 @@ function GetHTTPResponse([Uri] $Uri)
|
||||
})
|
||||
}
|
||||
|
||||
function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Coherent) {
|
||||
function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel) {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
$VersionFileUrl = $null
|
||||
@@ -269,17 +340,11 @@ function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Co
|
||||
elseif ($Runtime -eq "aspnetcore") {
|
||||
$VersionFileUrl = "$UncachedFeed/aspnetcore/Runtime/$Channel/latest.version"
|
||||
}
|
||||
# Currently, the WindowsDesktop runtime is manufactured with the .Net core runtime
|
||||
elseif ($Runtime -eq "windowsdesktop") {
|
||||
$VersionFileUrl = "$UncachedFeed/Runtime/$Channel/latest.version"
|
||||
$VersionFileUrl = "$UncachedFeed/WindowsDesktop/$Channel/latest.version"
|
||||
}
|
||||
elseif (-not $Runtime) {
|
||||
if ($Coherent) {
|
||||
$VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.coherent.version"
|
||||
}
|
||||
else {
|
||||
$VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.version"
|
||||
}
|
||||
$VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.version"
|
||||
}
|
||||
else {
|
||||
throw "Invalid value for `$Runtime"
|
||||
@@ -288,7 +353,8 @@ function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Co
|
||||
$Response = GetHTTPResponse -Uri $VersionFileUrl
|
||||
}
|
||||
catch {
|
||||
throw "Could not resolve version information."
|
||||
Say-Error "Could not resolve version information."
|
||||
throw
|
||||
}
|
||||
$StringContent = $Response.Content.ReadAsStringAsync().Result
|
||||
|
||||
@@ -314,7 +380,8 @@ function Parse-Jsonfile-For-Version([string]$JSonFile) {
|
||||
$JSonContent = Get-Content($JSonFile) -Raw | ConvertFrom-Json | Select-Object -expand "sdk" -ErrorAction SilentlyContinue
|
||||
}
|
||||
catch {
|
||||
throw "Json file unreadable: '$JSonFile'"
|
||||
Say-Error "Json file unreadable: '$JSonFile'"
|
||||
throw
|
||||
}
|
||||
if ($JSonContent) {
|
||||
try {
|
||||
@@ -327,7 +394,8 @@ function Parse-Jsonfile-For-Version([string]$JSonFile) {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
throw "Unable to parse the SDK node in '$JSonFile'"
|
||||
Say-Error "Unable to parse the SDK node in '$JSonFile'"
|
||||
throw
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -343,16 +411,12 @@ function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel,
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
if (-not $JSonFile) {
|
||||
switch ($Version.ToLower()) {
|
||||
{ $_ -eq "latest" } {
|
||||
$LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $False
|
||||
return $LatestVersionInfo.Version
|
||||
}
|
||||
{ $_ -eq "coherent" } {
|
||||
$LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $True
|
||||
return $LatestVersionInfo.Version
|
||||
}
|
||||
default { return $Version }
|
||||
if ($Version.ToLower() -eq "latest") {
|
||||
$LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel
|
||||
return $LatestVersionInfo.Version
|
||||
}
|
||||
else {
|
||||
return $Version
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -363,17 +427,29 @@ function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel,
|
||||
function Get-Download-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
# If anything fails in this lookup it will default to $SpecificVersion
|
||||
$SpecificProductVersion = Get-Product-Version -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion
|
||||
|
||||
if ($Runtime -eq "dotnet") {
|
||||
$PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificVersion-win-$CLIArchitecture.zip"
|
||||
$PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip"
|
||||
}
|
||||
elseif ($Runtime -eq "aspnetcore") {
|
||||
$PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificVersion-win-$CLIArchitecture.zip"
|
||||
$PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip"
|
||||
}
|
||||
elseif ($Runtime -eq "windowsdesktop") {
|
||||
$PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/windowsdesktop-runtime-$SpecificVersion-win-$CLIArchitecture.zip"
|
||||
# The windows desktop runtime is part of the core runtime layout prior to 5.0
|
||||
$PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip"
|
||||
if ($SpecificVersion -match '^(\d+)\.(.*)$')
|
||||
{
|
||||
$majorVersion = [int]$Matches[1]
|
||||
if ($majorVersion -ge 5)
|
||||
{
|
||||
$PayloadURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (-not $Runtime) {
|
||||
$PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificVersion-win-$CLIArchitecture.zip"
|
||||
$PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificProductVersion-win-$CLIArchitecture.zip"
|
||||
}
|
||||
else {
|
||||
throw "Invalid value for `$Runtime"
|
||||
@@ -381,7 +457,7 @@ function Get-Download-Link([string]$AzureFeed, [string]$SpecificVersion, [string
|
||||
|
||||
Say-Verbose "Constructed primary named payload URL: $PayloadURL"
|
||||
|
||||
return $PayloadURL
|
||||
return $PayloadURL, $SpecificProductVersion
|
||||
}
|
||||
|
||||
function Get-LegacyDownload-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) {
|
||||
@@ -402,6 +478,60 @@ function Get-LegacyDownload-Link([string]$AzureFeed, [string]$SpecificVersion, [
|
||||
return $PayloadURL
|
||||
}
|
||||
|
||||
function Get-Product-Version([string]$AzureFeed, [string]$SpecificVersion) {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
if ($Runtime -eq "dotnet") {
|
||||
$ProductVersionTxtURL = "$AzureFeed/Runtime/$SpecificVersion/productVersion.txt"
|
||||
}
|
||||
elseif ($Runtime -eq "aspnetcore") {
|
||||
$ProductVersionTxtURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/productVersion.txt"
|
||||
}
|
||||
elseif ($Runtime -eq "windowsdesktop") {
|
||||
# The windows desktop runtime is part of the core runtime layout prior to 5.0
|
||||
$ProductVersionTxtURL = "$AzureFeed/Runtime/$SpecificVersion/productVersion.txt"
|
||||
if ($SpecificVersion -match '^(\d+)\.(.*)')
|
||||
{
|
||||
$majorVersion = [int]$Matches[1]
|
||||
if ($majorVersion -ge 5)
|
||||
{
|
||||
$ProductVersionTxtURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/productVersion.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (-not $Runtime) {
|
||||
$ProductVersionTxtURL = "$AzureFeed/Sdk/$SpecificVersion/productVersion.txt"
|
||||
}
|
||||
else {
|
||||
throw "Invalid value '$Runtime' specified for `$Runtime"
|
||||
}
|
||||
|
||||
Say-Verbose "Checking for existence of $ProductVersionTxtURL"
|
||||
|
||||
try {
|
||||
$productVersionResponse = GetHTTPResponse($productVersionTxtUrl)
|
||||
|
||||
if ($productVersionResponse.StatusCode -eq 200) {
|
||||
$productVersion = $productVersionResponse.Content.ReadAsStringAsync().Result.Trim()
|
||||
if ($productVersion -ne $SpecificVersion)
|
||||
{
|
||||
Say "Using alternate version $productVersion found in $ProductVersionTxtURL"
|
||||
}
|
||||
|
||||
return $productVersion
|
||||
}
|
||||
else {
|
||||
Say-Verbose "Got StatusCode $($productVersionResponse.StatusCode) trying to get productVersion.txt at $productVersionTxtUrl, so using default value of $SpecificVersion"
|
||||
$productVersion = $SpecificVersion
|
||||
}
|
||||
} catch {
|
||||
Say-Verbose "Could not read productVersion.txt at $productVersionTxtUrl, so using default value of $SpecificVersion (Exception: '$($_.Exception.Message)' )"
|
||||
$productVersion = $SpecificVersion
|
||||
}
|
||||
|
||||
return $productVersion
|
||||
}
|
||||
|
||||
function Get-User-Share-Path() {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
@@ -539,6 +669,23 @@ function DownloadFile($Source, [string]$OutPath) {
|
||||
}
|
||||
}
|
||||
|
||||
function SafeRemoveFile($Path) {
|
||||
try {
|
||||
if (Test-Path $Path) {
|
||||
Remove-Item $Path
|
||||
Say-Verbose "The temporary file `"$Path`" was removed."
|
||||
}
|
||||
else
|
||||
{
|
||||
Say-Verbose "The temporary file `"$Path`" does not exist, therefore is not removed."
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Say-Warning "Failed to remove the temporary file: `"$Path`", remove it manually."
|
||||
}
|
||||
}
|
||||
|
||||
function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot, [string]$BinFolderRelativePath) {
|
||||
$BinPath = Get-Absolute-Path $(Join-Path -Path $InstallRoot -ChildPath $BinFolderRelativePath)
|
||||
if (-Not $NoPath) {
|
||||
@@ -555,9 +702,14 @@ function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot, [string]$BinFolde
|
||||
}
|
||||
}
|
||||
|
||||
Say "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:"
|
||||
Say "- The SDK needs to be installed without user interaction and without admin rights."
|
||||
Say "- The SDK installation doesn't need to persist across multiple CI runs."
|
||||
Say "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.`r`n"
|
||||
|
||||
$CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture
|
||||
$SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $AzureFeed -Channel $Channel -Version $Version -JSonFile $JSonFile
|
||||
$DownloadLink = Get-Download-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
|
||||
$DownloadLink, $EffectiveVersion = Get-Download-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
|
||||
$LegacyDownloadLink = Get-LegacyDownload-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
|
||||
|
||||
$InstallRoot = Resolve-Installation-Path $InstallDir
|
||||
@@ -583,7 +735,12 @@ if ($DryRun) {
|
||||
}
|
||||
}
|
||||
Say "Repeatable invocation: $RepeatableCommand"
|
||||
exit 0
|
||||
if ($SpecificVersion -ne $EffectiveVersion)
|
||||
{
|
||||
Say "NOTE: Due to finding a version manifest with this runtime, it would actually install with version '$EffectiveVersion'"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ($Runtime -eq "dotnet") {
|
||||
@@ -606,12 +763,18 @@ else {
|
||||
throw "Invalid value for `$Runtime"
|
||||
}
|
||||
|
||||
if ($SpecificVersion -ne $EffectiveVersion)
|
||||
{
|
||||
Say "Performing installation checks for effective version: $EffectiveVersion"
|
||||
$SpecificVersion = $EffectiveVersion
|
||||
}
|
||||
|
||||
# Check if the SDK version is already installed.
|
||||
$isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $SpecificVersion
|
||||
if ($isAssetInstalled) {
|
||||
Say "$assetName version $SpecificVersion is already installed."
|
||||
Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath
|
||||
exit 0
|
||||
return
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
|
||||
@@ -619,30 +782,69 @@ New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
|
||||
$installDrive = $((Get-Item $InstallRoot).PSDrive.Name);
|
||||
$diskInfo = Get-PSDrive -Name $installDrive
|
||||
if ($diskInfo.Free / 1MB -le 100) {
|
||||
Say "There is not enough disk space on drive ${installDrive}:"
|
||||
exit 0
|
||||
throw "There is not enough disk space on drive ${installDrive}:"
|
||||
}
|
||||
|
||||
$ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())
|
||||
Say-Verbose "Zip path: $ZipPath"
|
||||
|
||||
$DownloadFailed = $false
|
||||
Say "Downloading link: $DownloadLink"
|
||||
|
||||
$PrimaryDownloadStatusCode = 0
|
||||
$LegacyDownloadStatusCode = 0
|
||||
|
||||
$PrimaryDownloadFailedMsg = ""
|
||||
$LegacyDownloadFailedMsg = ""
|
||||
|
||||
Say "Downloading primary link $DownloadLink"
|
||||
try {
|
||||
DownloadFile -Source $DownloadLink -OutPath $ZipPath
|
||||
}
|
||||
catch {
|
||||
Say "Cannot download: $DownloadLink"
|
||||
if ($PSItem.Exception.Data.Contains("StatusCode")) {
|
||||
$PrimaryDownloadStatusCode = $PSItem.Exception.Data["StatusCode"]
|
||||
}
|
||||
|
||||
if ($PSItem.Exception.Data.Contains("ErrorMessage")) {
|
||||
$PrimaryDownloadFailedMsg = $PSItem.Exception.Data["ErrorMessage"]
|
||||
} else {
|
||||
$PrimaryDownloadFailedMsg = $PSItem.Exception.Message
|
||||
}
|
||||
|
||||
if ($PrimaryDownloadStatusCode -eq 404) {
|
||||
Say "The resource at $DownloadLink is not available."
|
||||
} else {
|
||||
Say $PSItem.Exception.Message
|
||||
}
|
||||
|
||||
SafeRemoveFile -Path $ZipPath
|
||||
|
||||
if ($LegacyDownloadLink) {
|
||||
$DownloadLink = $LegacyDownloadLink
|
||||
$ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())
|
||||
Say-Verbose "Legacy zip path: $ZipPath"
|
||||
Say "Downloading legacy link: $DownloadLink"
|
||||
Say "Downloading legacy link $DownloadLink"
|
||||
try {
|
||||
DownloadFile -Source $DownloadLink -OutPath $ZipPath
|
||||
}
|
||||
catch {
|
||||
Say "Cannot download: $DownloadLink"
|
||||
if ($PSItem.Exception.Data.Contains("StatusCode")) {
|
||||
$LegacyDownloadStatusCode = $PSItem.Exception.Data["StatusCode"]
|
||||
}
|
||||
|
||||
if ($PSItem.Exception.Data.Contains("ErrorMessage")) {
|
||||
$LegacyDownloadFailedMsg = $PSItem.Exception.Data["ErrorMessage"]
|
||||
} else {
|
||||
$LegacyDownloadFailedMsg = $PSItem.Exception.Message
|
||||
}
|
||||
|
||||
if ($LegacyDownloadStatusCode -eq 404) {
|
||||
Say "The resource at $DownloadLink is not available."
|
||||
} else {
|
||||
Say $PSItem.Exception.Message
|
||||
}
|
||||
|
||||
SafeRemoveFile -Path $ZipPath
|
||||
$DownloadFailed = $true
|
||||
}
|
||||
}
|
||||
@@ -652,7 +854,19 @@ catch {
|
||||
}
|
||||
|
||||
if ($DownloadFailed) {
|
||||
throw "Could not find/download: `"$assetName`" with version = $SpecificVersion`nRefer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support"
|
||||
if (($PrimaryDownloadStatusCode -eq 404) -and ((-not $LegacyDownloadLink) -or ($LegacyDownloadStatusCode -eq 404))) {
|
||||
throw "Could not find `"$assetName`" with version = $SpecificVersion`nRefer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support"
|
||||
} else {
|
||||
# 404-NotFound is an expected response if it goes from only one of the links, do not show that error.
|
||||
# If primary path is available (not 404-NotFound) then show the primary error else show the legacy error.
|
||||
if ($PrimaryDownloadStatusCode -ne 404) {
|
||||
throw "Could not download `"$assetName`" with version = $SpecificVersion`r`n$PrimaryDownloadFailedMsg"
|
||||
}
|
||||
if (($LegacyDownloadLink) -and ($LegacyDownloadStatusCode -ne 404)) {
|
||||
throw "Could not download `"$assetName`" with version = $SpecificVersion`r`n$LegacyDownloadFailedMsg"
|
||||
}
|
||||
throw "Could not download `"$assetName`" with version = $SpecificVersion"
|
||||
}
|
||||
}
|
||||
|
||||
Say "Extracting zip from $DownloadLink"
|
||||
@@ -674,13 +888,208 @@ if (!$isAssetInstalled) {
|
||||
$isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $SpecificVersion
|
||||
}
|
||||
|
||||
# Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
|
||||
if (!$isAssetInstalled) {
|
||||
Say-Error "Failed to verify the version of installed `"$assetName`".`nInstallation source: $DownloadLink.`nInstallation location: $InstallRoot.`nReport the bug at https://github.com/dotnet/install-scripts/issues."
|
||||
throw "`"$assetName`" with version = $SpecificVersion failed to install with an unknown error."
|
||||
}
|
||||
|
||||
Remove-Item $ZipPath
|
||||
SafeRemoveFile -Path $ZipPath
|
||||
|
||||
Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath
|
||||
|
||||
Say "Note that the script does not resolve dependencies during installation."
|
||||
Say "To check the list of dependencies, go to https://docs.microsoft.com/dotnet/core/install/windows#dependencies"
|
||||
Say "Installation finished"
|
||||
exit 0
|
||||
# SIG # Begin signature block
|
||||
# MIIjjwYJKoZIhvcNAQcCoIIjgDCCI3wCAQExDzANBglghkgBZQMEAgEFADB5Bgor
|
||||
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
|
||||
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCNsnhcJvx/hXmM
|
||||
# w8KjuvvIMDBFonhg9XJFc1QwfTyH4aCCDYEwggX/MIID56ADAgECAhMzAAABh3IX
|
||||
# chVZQMcJAAAAAAGHMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
|
||||
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
|
||||
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
|
||||
# bmcgUENBIDIwMTEwHhcNMjAwMzA0MTgzOTQ3WhcNMjEwMzAzMTgzOTQ3WjB0MQsw
|
||||
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
|
||||
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
|
||||
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
||||
# AQDOt8kLc7P3T7MKIhouYHewMFmnq8Ayu7FOhZCQabVwBp2VS4WyB2Qe4TQBT8aB
|
||||
# znANDEPjHKNdPT8Xz5cNali6XHefS8i/WXtF0vSsP8NEv6mBHuA2p1fw2wB/F0dH
|
||||
# sJ3GfZ5c0sPJjklsiYqPw59xJ54kM91IOgiO2OUzjNAljPibjCWfH7UzQ1TPHc4d
|
||||
# weils8GEIrbBRb7IWwiObL12jWT4Yh71NQgvJ9Fn6+UhD9x2uk3dLj84vwt1NuFQ
|
||||
# itKJxIV0fVsRNR3abQVOLqpDugbr0SzNL6o8xzOHL5OXiGGwg6ekiXA1/2XXY7yV
|
||||
# Fc39tledDtZjSjNbex1zzwSXAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
|
||||
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUhov4ZyO96axkJdMjpzu2zVXOJcsw
|
||||
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
|
||||
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDU4Mzg1MB8GA1UdIwQYMBaAFEhu
|
||||
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
|
||||
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
|
||||
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
|
||||
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
|
||||
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAixmy
|
||||
# S6E6vprWD9KFNIB9G5zyMuIjZAOuUJ1EK/Vlg6Fb3ZHXjjUwATKIcXbFuFC6Wr4K
|
||||
# NrU4DY/sBVqmab5AC/je3bpUpjtxpEyqUqtPc30wEg/rO9vmKmqKoLPT37svc2NV
|
||||
# BmGNl+85qO4fV/w7Cx7J0Bbqk19KcRNdjt6eKoTnTPHBHlVHQIHZpMxacbFOAkJr
|
||||
# qAVkYZdz7ikNXTxV+GRb36tC4ByMNxE2DF7vFdvaiZP0CVZ5ByJ2gAhXMdK9+usx
|
||||
# zVk913qKde1OAuWdv+rndqkAIm8fUlRnr4saSCg7cIbUwCCf116wUJ7EuJDg0vHe
|
||||
# yhnCeHnBbyH3RZkHEi2ofmfgnFISJZDdMAeVZGVOh20Jp50XBzqokpPzeZ6zc1/g
|
||||
# yILNyiVgE+RPkjnUQshd1f1PMgn3tns2Cz7bJiVUaqEO3n9qRFgy5JuLae6UweGf
|
||||
# AeOo3dgLZxikKzYs3hDMaEtJq8IP71cX7QXe6lnMmXU/Hdfz2p897Zd+kU+vZvKI
|
||||
# 3cwLfuVQgK2RZ2z+Kc3K3dRPz2rXycK5XCuRZmvGab/WbrZiC7wJQapgBodltMI5
|
||||
# GMdFrBg9IeF7/rP4EqVQXeKtevTlZXjpuNhhjuR+2DMt/dWufjXpiW91bo3aH6Ea
|
||||
# jOALXmoxgltCp1K7hrS6gmsvj94cLRf50QQ4U8Qwggd6MIIFYqADAgECAgphDpDS
|
||||
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
|
||||
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
|
||||
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
|
||||
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
|
||||
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
|
||||
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
|
||||
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
|
||||
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
|
||||
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
|
||||
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
|
||||
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
|
||||
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
|
||||
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
|
||||
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
|
||||
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
|
||||
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
|
||||
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
|
||||
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
|
||||
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
|
||||
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
|
||||
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
|
||||
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
|
||||
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
|
||||
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
|
||||
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
|
||||
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
|
||||
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
|
||||
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
|
||||
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
|
||||
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
|
||||
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
|
||||
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
|
||||
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
|
||||
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
|
||||
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
|
||||
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
|
||||
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
|
||||
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
|
||||
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
|
||||
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVZDCCFWACAQEwgZUwfjELMAkG
|
||||
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
|
||||
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
|
||||
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAYdyF3IVWUDHCQAAAAABhzAN
|
||||
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
|
||||
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgpT/bxWwe
|
||||
# aW0EinKMWCAzDXUjwXkIHldYzR6lw4/1Pc0wQgYKKwYBBAGCNwIBDDE0MDKgFIAS
|
||||
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
|
||||
# BgkqhkiG9w0BAQEFAASCAQCHd7sSQVq0YDg8QDx6/kLWn3s6jtvvIDCCgsO9spHM
|
||||
# quPd4FPbG67DCsKDClekQs52qrtRO3Zo+JMnCw4j3bS+gZHzeJr2shbftOrpsFoD
|
||||
# l7OPcUmtrqul9dkQCOp8t0MP3ls0n96/YyNy6lz4BAlTdkdDx957uAxalKaCIBzb
|
||||
# R9QyppOKIfNFvwD4EI5KI6tpmSy/uH8SrRg7ZExAYZl6J6R18WkL7KHn649lPoAQ
|
||||
# ujwrIXH10xOJops45ILGzKWQcHmCzLJGYapL4VHUuK+73nT+9ZROGHdk/PyvIcdw
|
||||
# iERa+C06v305t3DA+CuHFy1tvyw7IFF6RVbLZPwxrJjToYIS7jCCEuoGCisGAQQB
|
||||
# gjcDAwExghLaMIIS1gYJKoZIhvcNAQcCoIISxzCCEsMCAQMxDzANBglghkgBZQME
|
||||
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
|
||||
# MDEwDQYJYIZIAWUDBAIBBQAEIOCaTmvM1AP0WaEVqzKaaCu/R+bTlR4kCrM/ZXsb
|
||||
# /eNOAgZgGeLsMwsYEzIwMjEwMjAzMjExNzQ5LjU5MVowBIACAfSggdSkgdEwgc4x
|
||||
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
|
||||
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
|
||||
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
|
||||
# VFNTIEVTTjo4OTdBLUUzNTYtMTcwMTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
|
||||
# U3RhbXAgU2VydmljZaCCDkEwggT1MIID3aADAgECAhMzAAABLCKvRZd1+RvuAAAA
|
||||
# AAEsMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
|
||||
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
|
||||
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
|
||||
# MB4XDTE5MTIxOTAxMTUwM1oXDTIxMDMxNzAxMTUwM1owgc4xCzAJBgNVBAYTAlVT
|
||||
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
|
||||
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
|
||||
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo4OTdB
|
||||
# LUUzNTYtMTcwMTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
|
||||
# ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPK1zgSSq+MxAYo3qpCt
|
||||
# QDxSMPPJy6mm/wfEJNjNUnYtLFBwl1BUS5trEk/t41ldxITKehs+ABxYqo4Qxsg3
|
||||
# Gy1ugKiwHAnYiiekfC+ZhptNFgtnDZIn45zC0AlVr/6UfLtsLcHCh1XElLUHfEC0
|
||||
# nBuQcM/SpYo9e3l1qY5NdMgDGxCsmCKdiZfYXIu+U0UYIBhdzmSHnB3fxZOBVcr5
|
||||
# htFHEBBNt/rFJlm/A4yb8oBsp+Uf0p5QwmO/bCcdqB15JpylOhZmWs0sUfJKlK9E
|
||||
# rAhBwGki2eIRFKsQBdkXS9PWpF1w2gIJRvSkDEaCf+lbGTPdSzHSbfREWOF9wY3i
|
||||
# Yj8CAwEAAaOCARswggEXMB0GA1UdDgQWBBRRahZSGfrCQhCyIyGH9DkiaW7L0zAf
|
||||
# BgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBH
|
||||
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNU
|
||||
# aW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
|
||||
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0
|
||||
# YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsG
|
||||
# AQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQBPFxHIwi4vAH49w9Svmz6K3tM55RlW
|
||||
# 5pPeULXdut2Rqy6Ys0+VpZsbuaEoxs6Z1C3hMbkiqZFxxyltxJpuHTyGTg61zfNI
|
||||
# F5n6RsYF3s7IElDXNfZznF1/2iWc6uRPZK8rxxUJ/7emYXZCYwuUY0XjsCpP9pbR
|
||||
# RKeJi6r5arSyI+NfKxvgoM21JNt1BcdlXuAecdd/k8UjxCscffanoK2n6LFw1PcZ
|
||||
# lEO7NId7o+soM2C0QY5BYdghpn7uqopB6ixyFIIkDXFub+1E7GmAEwfU6VwEHL7y
|
||||
# 9rNE8bd+JrQs+yAtkkHy9FmXg/PsGq1daVzX1So7CJ6nyphpuHSN3VfTMIIGcTCC
|
||||
# BFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC
|
||||
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
|
||||
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv
|
||||
# b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcN
|
||||
# MjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
|
||||
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
|
||||
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIw
|
||||
# DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0
|
||||
# VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEw
|
||||
# RA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQe
|
||||
# dGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKx
|
||||
# Xf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4G
|
||||
# kbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEA
|
||||
# AaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7
|
||||
# fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
|
||||
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
|
||||
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
|
||||
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
|
||||
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
|
||||
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0g
|
||||
# AQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93
|
||||
# d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYB
|
||||
# BQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUA
|
||||
# bQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOh
|
||||
# IW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS
|
||||
# +7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlK
|
||||
# kVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon
|
||||
# /VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOi
|
||||
# PPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/
|
||||
# fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCII
|
||||
# YdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0
|
||||
# cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7a
|
||||
# KLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQ
|
||||
# cdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+
|
||||
# NR4Iuto229Nfj950iEkSoYICzzCCAjgCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYT
|
||||
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
|
||||
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBP
|
||||
# cGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo4
|
||||
# OTdBLUUzNTYtMTcwMTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
|
||||
# dmljZaIjCgEBMAcGBSsOAwIaAxUADE5OKSMoNx/mYxYWap1RTOohbJ2ggYMwgYCk
|
||||
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
|
||||
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
|
||||
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
|
||||
# AOPFChkwIhgPMjAyMTAyMDMxNTQwMDlaGA8yMDIxMDIwNDE1NDAwOVowdDA6Bgor
|
||||
# BgEEAYRZCgQBMSwwKjAKAgUA48UKGQIBADAHAgEAAgIXmDAHAgEAAgIRyTAKAgUA
|
||||
# 48ZbmQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID
|
||||
# B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAHeeznL2n6HWCjHH94Fl
|
||||
# hcdW6TEXzq4XNgp1Gx1W9F8gJ4x+SwoV7elJZkwgGffcpHomLvIY/VSuzsl1NgtJ
|
||||
# TWM2UxoqSv58BBOrl4eGhH6kkg8Ucy2tdeK5T8cHa8pMkq2j9pFd2mRG/6VMk0dl
|
||||
# Xz7Uy3Z6bZqkcABMyAfuAaGbMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMCVVMx
|
||||
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
|
||||
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
|
||||
# U3RhbXAgUENBIDIwMTACEzMAAAEsIq9Fl3X5G+4AAAAAASwwDQYJYIZIAWUDBAIB
|
||||
# BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx
|
||||
# IgQg/QYv7yp+354WTjWUIsXWndTEzXjaYjqwYjcBxCJKjdUwgfoGCyqGSIb3DQEJ
|
||||
# EAIvMYHqMIHnMIHkMIG9BCBbn/0uFFh42hTM5XOoKdXevBaiSxmYK9Ilcn9nu5ZH
|
||||
# 4TCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
|
||||
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
|
||||
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABLCKv
|
||||
# RZd1+RvuAAAAAAEsMCIEIIfIM3YbzHswb/Kj/qq1l1cHA6QBl+gEXYanUNJomrpT
|
||||
# MA0GCSqGSIb3DQEBCwUABIIBAAwdcXssUZGO7ho5+NHLjIxLtQk543aKGo+lrRMY
|
||||
# Q9abE1h/AaaNJl0iGxX4IihNWyfovSfYL3L4eODUBAu68tWSxeceRfWNsb/ZZfUi
|
||||
# v89hpLssI/Gf1BEgNMA4zCuIGQiC8okusVumEpAhhvCEbSiTTTtBdolTnU/CAKui
|
||||
# oxaU3R9XkKh1F4oAM26+dJ1J2BLQXPs5afNvvedDsZWNQUPK1sFF3JRfzxiTrwBW
|
||||
# EJRyflev9gyDoqCHzippgb+6+eti1WTkcA9Q49GIT11S6LOAVqkSC9N7Nqf8ksh8
|
||||
# ARdwT8jigpsm+mj7lrVU9upDkhVYhKeO8oiZq95Q53Zkteo=
|
||||
# SIG # End signature block
|
||||
|
||||
354
src/Misc/dotnet-install.sh
vendored
354
src/Misc/dotnet-install.sh
vendored
@@ -40,7 +40,7 @@ if [ -t 1 ] && command -v tput > /dev/null; then
|
||||
fi
|
||||
|
||||
say_warning() {
|
||||
printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}"
|
||||
printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}" >&3
|
||||
}
|
||||
|
||||
say_err() {
|
||||
@@ -172,7 +172,7 @@ get_current_os_name() {
|
||||
return 0
|
||||
elif [ "$uname" = "FreeBSD" ]; then
|
||||
echo "freebsd"
|
||||
return 0
|
||||
return 0
|
||||
elif [ "$uname" = "Linux" ]; then
|
||||
local linux_platform_name
|
||||
linux_platform_name="$(get_linux_platform_name)" || { echo "linux" && return 0 ; }
|
||||
@@ -183,6 +183,9 @@ get_current_os_name() {
|
||||
elif is_musl_based_distro; then
|
||||
echo "linux-musl"
|
||||
return 0
|
||||
elif [ "$linux_platform_name" = "linux-musl" ]; then
|
||||
echo "linux-musl"
|
||||
return 0
|
||||
else
|
||||
echo "linux"
|
||||
return 0
|
||||
@@ -241,42 +244,6 @@ check_min_reqs() {
|
||||
return 0
|
||||
}
|
||||
|
||||
check_pre_reqs() {
|
||||
eval $invocation
|
||||
|
||||
if [ "${DOTNET_INSTALL_SKIP_PREREQS:-}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$(uname)" = "Linux" ]; then
|
||||
if is_musl_based_distro; then
|
||||
if ! command -v scanelf > /dev/null; then
|
||||
say_warning "scanelf not found, please install pax-utils package."
|
||||
return 0
|
||||
fi
|
||||
LDCONFIG_COMMAND="scanelf --ldpath -BF '%f'"
|
||||
[ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep libintl)" ] && say_warning "Unable to locate libintl. Probable prerequisite missing; install libintl (or gettext)."
|
||||
else
|
||||
if [ ! -x "$(command -v ldconfig)" ]; then
|
||||
say_verbose "ldconfig is not in PATH, trying /sbin/ldconfig."
|
||||
LDCONFIG_COMMAND="/sbin/ldconfig"
|
||||
else
|
||||
LDCONFIG_COMMAND="ldconfig"
|
||||
fi
|
||||
local librarypath=${LD_LIBRARY_PATH:-}
|
||||
LDCONFIG_COMMAND="$LDCONFIG_COMMAND -NXv ${librarypath//:/ }"
|
||||
fi
|
||||
|
||||
[ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep zlib)" ] && say_warning "Unable to locate zlib. Probable prerequisite missing; install zlib."
|
||||
[ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep ssl)" ] && say_warning "Unable to locate libssl. Probable prerequisite missing; install libssl."
|
||||
[ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep libicu)" ] && say_warning "Unable to locate libicu. Probable prerequisite missing; install libicu."
|
||||
[ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep lttng)" ] && say_warning "Unable to locate liblttng. Probable prerequisite missing; install libcurl."
|
||||
[ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep libcurl)" ] && say_warning "Unable to locate libcurl. Probable prerequisite missing; install libcurl."
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# args:
|
||||
# input - $1
|
||||
to_lowercase() {
|
||||
@@ -332,11 +299,11 @@ get_machine_architecture() {
|
||||
if command -v uname > /dev/null; then
|
||||
CPUName=$(uname -m)
|
||||
case $CPUName in
|
||||
armv7l)
|
||||
armv*l)
|
||||
echo "arm"
|
||||
return 0
|
||||
;;
|
||||
aarch64)
|
||||
aarch64|arm64)
|
||||
echo "arm64"
|
||||
return 0
|
||||
;;
|
||||
@@ -373,10 +340,34 @@ get_normalized_architecture_from_architecture() {
|
||||
;;
|
||||
esac
|
||||
|
||||
say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/sdk/issues"
|
||||
say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues"
|
||||
return 1
|
||||
}
|
||||
|
||||
# args:
|
||||
# user_defined_os - $1
|
||||
get_normalized_os() {
|
||||
eval $invocation
|
||||
|
||||
local osname="$(to_lowercase "$1")"
|
||||
if [ ! -z "$osname" ]; then
|
||||
case "$osname" in
|
||||
osx | freebsd | rhel.6 | linux-musl | linux)
|
||||
echo "$osname"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
osname="$(get_current_os_name)" || return 1
|
||||
fi
|
||||
echo "$osname"
|
||||
return 0
|
||||
}
|
||||
|
||||
# The version text returned from the feeds is a 1-line or 2-line string:
|
||||
# For the SDK and the dotnet runtime (2 lines):
|
||||
# Line 1: # commit_hash
|
||||
@@ -418,14 +409,12 @@ is_dotnet_package_installed() {
|
||||
# azure_feed - $1
|
||||
# channel - $2
|
||||
# normalized_architecture - $3
|
||||
# coherent - $4
|
||||
get_latest_version_info() {
|
||||
eval $invocation
|
||||
|
||||
local azure_feed="$1"
|
||||
local channel="$2"
|
||||
local normalized_architecture="$3"
|
||||
local coherent="$4"
|
||||
|
||||
local version_file_url=null
|
||||
if [[ "$runtime" == "dotnet" ]]; then
|
||||
@@ -433,11 +422,7 @@ get_latest_version_info() {
|
||||
elif [[ "$runtime" == "aspnetcore" ]]; then
|
||||
version_file_url="$uncached_feed/aspnetcore/Runtime/$channel/latest.version"
|
||||
elif [ -z "$runtime" ]; then
|
||||
if [ "$coherent" = true ]; then
|
||||
version_file_url="$uncached_feed/Sdk/$channel/latest.coherent.version"
|
||||
else
|
||||
version_file_url="$uncached_feed/Sdk/$channel/latest.version"
|
||||
fi
|
||||
version_file_url="$uncached_feed/Sdk/$channel/latest.version"
|
||||
else
|
||||
say_err "Invalid value for \$runtime"
|
||||
return 1
|
||||
@@ -468,7 +453,6 @@ parse_jsonfile_for_version() {
|
||||
sdk_list=$(echo $sdk_section | awk -F"[{}]" '{print $2}')
|
||||
sdk_list=${sdk_list//[\" ]/}
|
||||
sdk_list=${sdk_list//,/$'\n'}
|
||||
sdk_list="$(echo -e "${sdk_list}" | tr -d '[[:space:]]')"
|
||||
|
||||
local version_info=""
|
||||
while read -r line; do
|
||||
@@ -505,26 +489,16 @@ get_specific_version_from_version() {
|
||||
local json_file="$5"
|
||||
|
||||
if [ -z "$json_file" ]; then
|
||||
case "$version" in
|
||||
latest)
|
||||
local version_info
|
||||
version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1
|
||||
say_verbose "get_specific_version_from_version: version_info=$version_info"
|
||||
echo "$version_info" | get_version_from_version_info
|
||||
return 0
|
||||
;;
|
||||
coherent)
|
||||
local version_info
|
||||
version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" true)" || return 1
|
||||
say_verbose "get_specific_version_from_version: version_info=$version_info"
|
||||
echo "$version_info" | get_version_from_version_info
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "$version"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
if [[ "$version" == "latest" ]]; then
|
||||
local version_info
|
||||
version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1
|
||||
say_verbose "get_specific_version_from_version: version_info=$version_info"
|
||||
echo "$version_info" | get_version_from_version_info
|
||||
return 0
|
||||
else
|
||||
echo "$version"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
local version_info
|
||||
version_info="$(parse_jsonfile_for_version "$json_file")" || return 1
|
||||
@@ -538,6 +512,7 @@ get_specific_version_from_version() {
|
||||
# channel - $2
|
||||
# normalized_architecture - $3
|
||||
# specific_version - $4
|
||||
# normalized_os - $5
|
||||
construct_download_link() {
|
||||
eval $invocation
|
||||
|
||||
@@ -545,17 +520,16 @@ construct_download_link() {
|
||||
local channel="$2"
|
||||
local normalized_architecture="$3"
|
||||
local specific_version="${4//[$'\t\r\n']}"
|
||||
|
||||
local osname
|
||||
osname="$(get_current_os_name)" || return 1
|
||||
local specific_product_version="$(get_specific_product_version "$1" "$4")"
|
||||
local osname="$5"
|
||||
|
||||
local download_link=null
|
||||
if [[ "$runtime" == "dotnet" ]]; then
|
||||
download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_version-$osname-$normalized_architecture.tar.gz"
|
||||
download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz"
|
||||
elif [[ "$runtime" == "aspnetcore" ]]; then
|
||||
download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_version-$osname-$normalized_architecture.tar.gz"
|
||||
download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz"
|
||||
elif [ -z "$runtime" ]; then
|
||||
download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_version-$osname-$normalized_architecture.tar.gz"
|
||||
download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_product_version-$osname-$normalized_architecture.tar.gz"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
@@ -564,6 +538,50 @@ construct_download_link() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# args:
|
||||
# azure_feed - $1
|
||||
# specific_version - $2
|
||||
get_specific_product_version() {
|
||||
# If we find a 'productVersion.txt' at the root of any folder, we'll use its contents
|
||||
# to resolve the version of what's in the folder, superseding the specified version.
|
||||
eval $invocation
|
||||
|
||||
local azure_feed="$1"
|
||||
local specific_version="${2//[$'\t\r\n']}"
|
||||
local specific_product_version=$specific_version
|
||||
|
||||
local download_link=null
|
||||
if [[ "$runtime" == "dotnet" ]]; then
|
||||
download_link="$azure_feed/Runtime/$specific_version/productVersion.txt${feed_credential}"
|
||||
elif [[ "$runtime" == "aspnetcore" ]]; then
|
||||
download_link="$azure_feed/aspnetcore/Runtime/$specific_version/productVersion.txt${feed_credential}"
|
||||
elif [ -z "$runtime" ]; then
|
||||
download_link="$azure_feed/Sdk/$specific_version/productVersion.txt${feed_credential}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
if machine_has "curl"
|
||||
then
|
||||
specific_product_version=$(curl -s --fail "$download_link")
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
specific_product_version=$specific_version
|
||||
fi
|
||||
elif machine_has "wget"
|
||||
then
|
||||
specific_product_version=$(wget -qO- "$download_link")
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
specific_product_version=$specific_version
|
||||
fi
|
||||
fi
|
||||
specific_product_version="${specific_product_version//[$'\t\r\n']}"
|
||||
|
||||
echo "$specific_product_version"
|
||||
return 0
|
||||
}
|
||||
|
||||
# args:
|
||||
# azure_feed - $1
|
||||
# channel - $2
|
||||
@@ -684,11 +702,31 @@ extract_dotnet_package() {
|
||||
find "$temp_out_path" -type f | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files"
|
||||
|
||||
rm -rf "$temp_out_path"
|
||||
rm -f "$zip_path" && say_verbose "Temporary zip file $zip_path was removed"
|
||||
|
||||
if [ "$failed" = true ]; then
|
||||
say_err "Extraction failed"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
get_http_header_curl() {
|
||||
eval $invocation
|
||||
local remote_path="$1"
|
||||
remote_path_with_credential="${remote_path}${feed_credential}"
|
||||
curl_options="-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 "
|
||||
curl $curl_options "$remote_path_with_credential" || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
get_http_header_wget() {
|
||||
eval $invocation
|
||||
local remote_path="$1"
|
||||
remote_path_with_credential="${remote_path}${feed_credential}"
|
||||
wget_options="-q -S --spider --tries 5 --waitretry 2 --connect-timeout 15 "
|
||||
wget $wget_options "$remote_path_with_credential" 2>&1 || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# args:
|
||||
@@ -706,13 +744,30 @@ download() {
|
||||
fi
|
||||
|
||||
local failed=false
|
||||
if machine_has "curl"; then
|
||||
downloadcurl "$remote_path" "$out_path" || failed=true
|
||||
elif machine_has "wget"; then
|
||||
downloadwget "$remote_path" "$out_path" || failed=true
|
||||
else
|
||||
failed=true
|
||||
fi
|
||||
local attempts=0
|
||||
while [ $attempts -lt 3 ]; do
|
||||
attempts=$((attempts+1))
|
||||
failed=false
|
||||
if machine_has "curl"; then
|
||||
downloadcurl "$remote_path" "$out_path" || failed=true
|
||||
elif machine_has "wget"; then
|
||||
downloadwget "$remote_path" "$out_path" || failed=true
|
||||
else
|
||||
say_err "Missing dependency: neither curl nor wget was found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$failed" = false ] || [ $attempts -ge 3 ] || { [ ! -z $http_code ] && [ $http_code = "404" ]; }; then
|
||||
break
|
||||
fi
|
||||
|
||||
say "Download attempt #$attempts has failed: $http_code $download_error_msg"
|
||||
say "Attempt #$((attempts+1)) will start in $((attempts*10)) seconds."
|
||||
sleep $((attempts*20))
|
||||
done
|
||||
|
||||
|
||||
|
||||
if [ "$failed" = true ]; then
|
||||
say_verbose "Download failed: $remote_path"
|
||||
return 1
|
||||
@@ -720,43 +775,60 @@ download() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Updates global variables $http_code and $download_error_msg
|
||||
downloadcurl() {
|
||||
eval $invocation
|
||||
unset http_code
|
||||
unset download_error_msg
|
||||
local remote_path="$1"
|
||||
local out_path="${2:-}"
|
||||
|
||||
# Append feed_credential as late as possible before calling curl to avoid logging feed_credential
|
||||
remote_path="${remote_path}${feed_credential}"
|
||||
|
||||
local remote_path_with_credential="${remote_path}${feed_credential}"
|
||||
local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs "
|
||||
local failed=false
|
||||
if [ -z "$out_path" ]; then
|
||||
curl --retry 10 -sSL -f --create-dirs "$remote_path" || failed=true
|
||||
curl $curl_options "$remote_path_with_credential" || failed=true
|
||||
else
|
||||
curl --retry 10 -sSL -f --create-dirs -o "$out_path" "$remote_path" || failed=true
|
||||
curl $curl_options -o "$out_path" "$remote_path_with_credential" || failed=true
|
||||
fi
|
||||
if [ "$failed" = true ]; then
|
||||
say_verbose "Curl download failed"
|
||||
local response=$(get_http_header_curl $remote_path_with_credential)
|
||||
http_code=$( echo "$response" | awk '/^HTTP/{print $2}' | tail -1 )
|
||||
download_error_msg="Unable to download $remote_path."
|
||||
if [[ $http_code != 2* ]]; then
|
||||
download_error_msg+=" Returned HTTP status code: $http_code."
|
||||
fi
|
||||
say_verbose "$download_error_msg"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# Updates global variables $http_code and $download_error_msg
|
||||
downloadwget() {
|
||||
eval $invocation
|
||||
unset http_code
|
||||
unset download_error_msg
|
||||
local remote_path="$1"
|
||||
local out_path="${2:-}"
|
||||
|
||||
# Append feed_credential as late as possible before calling wget to avoid logging feed_credential
|
||||
remote_path="${remote_path}${feed_credential}"
|
||||
|
||||
local remote_path_with_credential="${remote_path}${feed_credential}"
|
||||
local wget_options="--tries 20 --waitretry 2 --connect-timeout 15 "
|
||||
local failed=false
|
||||
if [ -z "$out_path" ]; then
|
||||
wget -q --tries 10 -O - "$remote_path" || failed=true
|
||||
wget -q $wget_options -O - "$remote_path_with_credential" || failed=true
|
||||
else
|
||||
wget --tries 10 -O "$out_path" "$remote_path" || failed=true
|
||||
wget $wget_options -O "$out_path" "$remote_path_with_credential" || failed=true
|
||||
fi
|
||||
if [ "$failed" = true ]; then
|
||||
say_verbose "Wget download failed"
|
||||
local response=$(get_http_header_wget $remote_path_with_credential)
|
||||
http_code=$( echo "$response" | awk '/^ HTTP/{print $2}' | tail -1 )
|
||||
download_error_msg="Unable to download $remote_path."
|
||||
if [[ $http_code != 2* ]]; then
|
||||
download_error_msg+=" Returned HTTP status code: $http_code."
|
||||
fi
|
||||
say_verbose "$download_error_msg"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
@@ -769,14 +841,18 @@ calculate_vars() {
|
||||
normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")"
|
||||
say_verbose "normalized_architecture=$normalized_architecture"
|
||||
|
||||
normalized_os="$(get_normalized_os "$user_defined_os")"
|
||||
say_verbose "normalized_os=$normalized_os"
|
||||
|
||||
specific_version="$(get_specific_version_from_version "$azure_feed" "$channel" "$normalized_architecture" "$version" "$json_file")"
|
||||
specific_product_version="$(get_specific_product_version "$azure_feed" "$specific_version")"
|
||||
say_verbose "specific_version=$specific_version"
|
||||
if [ -z "$specific_version" ]; then
|
||||
say_err "Could not resolve version information."
|
||||
return 1
|
||||
fi
|
||||
|
||||
download_link="$(construct_download_link "$azure_feed" "$channel" "$normalized_architecture" "$specific_version")"
|
||||
download_link="$(construct_download_link "$azure_feed" "$channel" "$normalized_architecture" "$specific_version" "$normalized_os")"
|
||||
say_verbose "Constructed primary named payload URL: $download_link"
|
||||
|
||||
legacy_download_link="$(construct_legacy_download_link "$azure_feed" "$channel" "$normalized_architecture" "$specific_version")" || valid_legacy_download_link=false
|
||||
@@ -821,38 +897,74 @@ install_dotnet() {
|
||||
zip_path="$(mktemp "$temporary_file_template")"
|
||||
say_verbose "Zip path: $zip_path"
|
||||
|
||||
say "Downloading link: $download_link"
|
||||
|
||||
# Failures are normal in the non-legacy case for ultimately legacy downloads.
|
||||
# Do not output to stderr, since output to stderr is considered an error.
|
||||
say "Downloading primary link $download_link"
|
||||
|
||||
# The download function will set variables $http_code and $download_error_msg in case of failure.
|
||||
download "$download_link" "$zip_path" 2>&1 || download_failed=true
|
||||
|
||||
# if the download fails, download the legacy_download_link
|
||||
if [ "$download_failed" = true ]; then
|
||||
say "Cannot download: $download_link"
|
||||
|
||||
primary_path_http_code="$http_code"; primary_path_download_error_msg="$download_error_msg"
|
||||
case $primary_path_http_code in
|
||||
404)
|
||||
say "The resource at $download_link is not available."
|
||||
;;
|
||||
*)
|
||||
say "$primary_path_download_error_msg"
|
||||
;;
|
||||
esac
|
||||
rm -f "$zip_path" 2>&1 && say_verbose "Temporary zip file $zip_path was removed"
|
||||
if [ "$valid_legacy_download_link" = true ]; then
|
||||
download_failed=false
|
||||
download_link="$legacy_download_link"
|
||||
zip_path="$(mktemp "$temporary_file_template")"
|
||||
say_verbose "Legacy zip path: $zip_path"
|
||||
say "Downloading legacy link: $download_link"
|
||||
|
||||
say "Downloading legacy link $download_link"
|
||||
|
||||
# The download function will set variables $http_code and $download_error_msg in case of failure.
|
||||
download "$download_link" "$zip_path" 2>&1 || download_failed=true
|
||||
|
||||
if [ "$download_failed" = true ]; then
|
||||
say "Cannot download: $download_link"
|
||||
legacy_path_http_code="$http_code"; legacy_path_download_error_msg="$download_error_msg"
|
||||
case $legacy_path_http_code in
|
||||
404)
|
||||
say "The resource at $download_link is not available."
|
||||
;;
|
||||
*)
|
||||
say "$legacy_path_download_error_msg"
|
||||
;;
|
||||
esac
|
||||
rm -f "$zip_path" 2>&1 && say_verbose "Temporary zip file $zip_path was removed"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$download_failed" = true ]; then
|
||||
say_err "Could not find/download: \`$asset_name\` with version = $specific_version"
|
||||
say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support"
|
||||
if [[ "$primary_path_http_code" = "404" && ( "$valid_legacy_download_link" = false || "$legacy_path_http_code" = "404") ]]; then
|
||||
say_err "Could not find \`$asset_name\` with version = $specific_version"
|
||||
say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support"
|
||||
else
|
||||
say_err "Could not download: \`$asset_name\` with version = $specific_version"
|
||||
# 404-NotFound is an expected response if it goes from only one of the links, do not show that error.
|
||||
# If primary path is available (not 404-NotFound) then show the primary error else show the legacy error.
|
||||
if [ "$primary_path_http_code" != "404" ]; then
|
||||
say_err "$primary_path_download_error_msg"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$valid_legacy_download_link" = true && "$legacy_path_http_code" != "404" ]]; then
|
||||
say_err "$legacy_path_download_error_msg"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
say "Extracting zip from $download_link"
|
||||
extract_dotnet_package "$zip_path" "$install_root"
|
||||
extract_dotnet_package "$zip_path" "$install_root" || return 1
|
||||
|
||||
# Check if the SDK version is installed; if not, fail the installation.
|
||||
# if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed.
|
||||
@@ -868,12 +980,14 @@ install_dotnet() {
|
||||
fi
|
||||
|
||||
# Check if the standard SDK version is installed.
|
||||
say_verbose "Checking installation: version = $specific_version"
|
||||
if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$specific_version"; then
|
||||
say_verbose "Checking installation: version = $specific_product_version"
|
||||
if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$specific_product_version"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
say_err "\`$asset_name\` with version = $specific_version failed to install with an unknown error."
|
||||
# Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
|
||||
say_err "Failed to verify the version of installed \`$asset_name\`.\nInstallation source: $download_link.\nInstallation location: $install_root.\nReport the bug at https://github.com/dotnet/install-scripts/issues."
|
||||
say_err "\`$asset_name\` with version = $specific_product_version failed to install with an unknown error."
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -899,6 +1013,7 @@ runtime=""
|
||||
runtime_id=""
|
||||
override_non_versioned_files=true
|
||||
non_dynamic_parameters=""
|
||||
user_defined_os=""
|
||||
|
||||
while [ $# -ne 0 ]
|
||||
do
|
||||
@@ -920,6 +1035,10 @@ do
|
||||
shift
|
||||
architecture="$1"
|
||||
;;
|
||||
--os|-[Oo][SS])
|
||||
shift
|
||||
user_defined_os="$1"
|
||||
;;
|
||||
--shared-runtime|-[Ss]hared[Rr]untime)
|
||||
say_warning "The --shared-runtime flag is obsolete and may be removed in a future version of this script. The recommended usage is to specify '--runtime dotnet'."
|
||||
if [ -z "$runtime" ]; then
|
||||
@@ -971,6 +1090,7 @@ do
|
||||
shift
|
||||
runtime_id="$1"
|
||||
non_dynamic_parameters+=" $name "\""$1"\"""
|
||||
say_warning "Use of --runtime-id is obsolete and should be limited to the versions below 2.1. To override architecture, use --architecture option instead. To override OS, use --os option instead."
|
||||
;;
|
||||
--jsonfile|-[Jj][Ss]on[Ff]ile)
|
||||
shift
|
||||
@@ -1003,8 +1123,6 @@ do
|
||||
echo " -Version"
|
||||
echo " Possible values:"
|
||||
echo " - latest - most latest build on specific channel"
|
||||
echo " - coherent - most latest coherent build on specific channel"
|
||||
echo " coherent applies only to SDK downloads"
|
||||
echo " - 3-part version in a format A.B.C - represents specific version of build"
|
||||
echo " examples: 2.0.0-preview2-006120; 1.1.0"
|
||||
echo " -i,--install-dir <DIR> Install under specified location (see Install Location below)"
|
||||
@@ -1012,6 +1130,11 @@ do
|
||||
echo " --architecture <ARCHITECTURE> Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`."
|
||||
echo " --arch,-Architecture,-Arch"
|
||||
echo " Possible values: x64, arm, and arm64"
|
||||
echo " --os <system> Specifies operating system to be used when selecting the installer."
|
||||
echo " Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6."
|
||||
echo " In case any other value is provided, the platform will be determined by the script based on machine configuration."
|
||||
echo " Not supported for legacy links. Use --runtime-id to specify platform for legacy links."
|
||||
echo " Refer to: https://aka.ms/dotnet-os-lifecycle for more information."
|
||||
echo " --runtime <RUNTIME> Installs a shared runtime only, without the SDK."
|
||||
echo " -Runtime"
|
||||
echo " Possible values:"
|
||||
@@ -1028,14 +1151,15 @@ do
|
||||
echo " --no-cdn,-NoCdn Disable downloading from the Azure CDN, and use the uncached feed directly."
|
||||
echo " --jsonfile <JSONFILE> Determines the SDK version from a user specified global.json file."
|
||||
echo " Note: global.json must have a value for 'SDK:Version'"
|
||||
echo " --runtime-id Installs the .NET Tools for the given platform (use linux-x64 for portable linux)."
|
||||
echo " -RuntimeId"
|
||||
echo " -?,--?,-h,--help,-Help Shows this help message"
|
||||
echo ""
|
||||
echo "Obsolete parameters:"
|
||||
echo " --shared-runtime The recommended alternative is '--runtime dotnet'."
|
||||
echo " This parameter is obsolete and may be removed in a future version of this script."
|
||||
echo " Installs just the shared runtime bits, not the entire SDK."
|
||||
echo " --runtime-id Installs the .NET Tools for the given platform (use linux-x64 for portable linux)."
|
||||
echo " -RuntimeId" The parameter is obsolete and may be removed in a future version of this script. Should be used only for versions below 2.1.
|
||||
echo " For primary links to override OS or/and architecture, use --os and --architecture option instead."
|
||||
echo ""
|
||||
echo "Install Location:"
|
||||
echo " Location is chosen in following order:"
|
||||
@@ -1057,6 +1181,11 @@ if [ "$no_cdn" = true ]; then
|
||||
azure_feed="$uncached_feed"
|
||||
fi
|
||||
|
||||
say "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:"
|
||||
say "- The SDK needs to be installed without user interaction and without admin rights."
|
||||
say "- The SDK installation doesn't need to persist across multiple CI runs."
|
||||
say "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.\n"
|
||||
|
||||
check_min_reqs
|
||||
calculate_vars
|
||||
script_name=$(basename "$0")
|
||||
@@ -1067,7 +1196,7 @@ if [ "$dry_run" = true ]; then
|
||||
if [ "$valid_legacy_download_link" = true ]; then
|
||||
say "Legacy named payload URL: $legacy_download_link"
|
||||
fi
|
||||
repeatable_command="./$script_name --version "\""$specific_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\"""
|
||||
repeatable_command="./$script_name --version "\""$specific_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\"" --os "\""$normalized_os"\"""
|
||||
if [[ "$runtime" == "dotnet" ]]; then
|
||||
repeatable_command+=" --runtime "\""dotnet"\"""
|
||||
elif [[ "$runtime" == "aspnetcore" ]]; then
|
||||
@@ -1078,7 +1207,6 @@ if [ "$dry_run" = true ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_pre_reqs
|
||||
install_dotnet
|
||||
|
||||
bin_path="$(get_absolute_path "$(combine_paths "$install_root" "$bin_folder_relative_path")")"
|
||||
@@ -1089,4 +1217,6 @@ else
|
||||
say "Binaries of dotnet can be found in $bin_path"
|
||||
fi
|
||||
|
||||
say "Note that the script does not resolve dependencies during installation."
|
||||
say "To check the list of dependencies, go to https://docs.microsoft.com/dotnet/core/install, select your operating system and check the \"Dependencies\" section."
|
||||
say "Installation finished successfully."
|
||||
|
||||
1095
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
1095
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@
|
||||
"@types/node": "^12.7.12",
|
||||
"@typescript-eslint/parser": "^2.8.0",
|
||||
"@zeit/ncc": "^0.20.5",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-github": "^2.0.0",
|
||||
"prettier": "^1.19.1",
|
||||
"typescript": "^3.6.4"
|
||||
|
||||
@@ -3,7 +3,8 @@ PACKAGERUNTIME=$1
|
||||
PRECACHE=$2
|
||||
|
||||
NODE_URL=https://nodejs.org/dist
|
||||
NODE12_VERSION="12.13.1"
|
||||
NODE12_VERSION="12.22.7"
|
||||
NODE16_VERSION="16.13.0"
|
||||
|
||||
get_abs_path() {
|
||||
# exploits the fact that pwd will print abs path when no args
|
||||
@@ -126,6 +127,8 @@ function acquireExternalTool() {
|
||||
if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then
|
||||
acquireExternalTool "$NODE_URL/v${NODE12_VERSION}/$PACKAGERUNTIME/node.exe" node12/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE12_VERSION}/$PACKAGERUNTIME/node.lib" node12/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/$PACKAGERUNTIME/node.exe" node16/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/$PACKAGERUNTIME/node.lib" node16/bin
|
||||
if [[ "$PRECACHE" != "" ]]; then
|
||||
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
|
||||
fi
|
||||
@@ -134,18 +137,23 @@ fi
|
||||
# Download the external tools only for OSX.
|
||||
if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then
|
||||
acquireExternalTool "$NODE_URL/v${NODE12_VERSION}/node-v${NODE12_VERSION}-darwin-x64.tar.gz" node12 fix_nested_dir
|
||||
acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-darwin-x64.tar.gz" node16 fix_nested_dir
|
||||
fi
|
||||
|
||||
# Download the external tools for Linux PACKAGERUNTIMEs.
|
||||
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
|
||||
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 "https://vstsagenttools.blob.core.windows.net/tools/nodejs/${NODE16_VERSION}/alpine/x64/node-v${NODE16_VERSION}-alpine-x64.tar.gz" node16_alpine
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGERUNTIME" == "linux-arm64" ]]; then
|
||||
acquireExternalTool "$NODE_URL/v${NODE12_VERSION}/node-v${NODE12_VERSION}-linux-arm64.tar.gz" node12 fix_nested_dir
|
||||
acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-linux-arm64.tar.gz" node16 fix_nested_dir
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then
|
||||
acquireExternalTool "$NODE_URL/v${NODE12_VERSION}/node-v${NODE12_VERSION}-linux-armv7l.tar.gz" node12 fix_nested_dir
|
||||
acquireExternalTool "$NODE_URL/v${NODE16_VERSION}/node-v${NODE16_VERSION}-linux-armv7l.tar.gz" node16 fix_nested_dir
|
||||
fi
|
||||
|
||||
@@ -16,11 +16,11 @@ if (supported.indexOf(process.platform) == -1) {
|
||||
var stopping = false;
|
||||
var listener = null;
|
||||
|
||||
var runService = function() {
|
||||
var runService = function () {
|
||||
var listenerExePath = path.join(__dirname, '../bin/Runner.Listener');
|
||||
var interactive = process.argv[2] === "interactive";
|
||||
|
||||
if(!stopping) {
|
||||
if (!stopping) {
|
||||
try {
|
||||
if (interactive) {
|
||||
console.log('Starting Runner listener interactively');
|
||||
@@ -30,8 +30,8 @@ var runService = function() {
|
||||
listener = childProcess.spawn(listenerExePath, ['run', '--startuptype', 'service'], { env: process.env });
|
||||
}
|
||||
|
||||
console.log('Started listener process');
|
||||
|
||||
console.log(`Started listener process, pid: ${listener.pid}`);
|
||||
|
||||
listener.stdout.on('data', (data) => {
|
||||
process.stdout.write(data.toString('utf8'));
|
||||
});
|
||||
@@ -40,6 +40,10 @@ var runService = function() {
|
||||
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}`);
|
||||
|
||||
@@ -56,13 +60,13 @@ var runService = function() {
|
||||
} else {
|
||||
console.log('Runner listener exit with undefined return code, re-launch runner in 5 seconds.');
|
||||
}
|
||||
|
||||
if(!stopping) {
|
||||
|
||||
if (!stopping) {
|
||||
setTimeout(runService, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
} catch(ex) {
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
}
|
||||
}
|
||||
@@ -71,14 +75,15 @@ var runService = function() {
|
||||
runService();
|
||||
console.log('Started running service');
|
||||
|
||||
var gracefulShutdown = function(code) {
|
||||
var gracefulShutdown = function (code) {
|
||||
console.log('Shutting down runner listener');
|
||||
stopping = true;
|
||||
if (listener) {
|
||||
console.log('Sending SIGINT to runner listener to stop');
|
||||
listener.kill('SIGINT');
|
||||
|
||||
// TODO wait for 30 seconds and send a SIGKILL
|
||||
console.log('Sending SIGKILL to runner listener');
|
||||
setTimeout(() => listener.kill('SIGKILL'), 30000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,5 +23,9 @@
|
||||
<key>ACTIONS_RUNNER_SVC</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
<key>ProcessType</key>
|
||||
<string>Interactive</string>
|
||||
<key>SessionCreate</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
115
src/Misc/layoutbin/checkScripts/downloadCert.js
Normal file
115
src/Misc/layoutbin/checkScripts/downloadCert.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const https = require('https')
|
||||
const fs = require('fs')
|
||||
const http = require('http')
|
||||
const hostname = process.env['HOSTNAME'] || ''
|
||||
const port = process.env['PORT'] || ''
|
||||
const path = process.env['PATH'] || ''
|
||||
const pat = process.env['PAT'] || ''
|
||||
const proxyHost = process.env['PROXYHOST'] || ''
|
||||
const proxyPort = process.env['PROXYPORT'] || ''
|
||||
const proxyUsername = process.env['PROXYUSERNAME'] || ''
|
||||
const proxyPassword = process.env['PROXYPASSWORD'] || ''
|
||||
|
||||
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'
|
||||
|
||||
if (proxyHost === '') {
|
||||
const options = {
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
path: path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'GitHubActionsRunnerCheck/1.0',
|
||||
'Authorization': `token ${pat}`
|
||||
},
|
||||
}
|
||||
const req = https.request(options, res => {
|
||||
console.log(`statusCode: ${res.statusCode}`)
|
||||
console.log(`headers: ${JSON.stringify(res.headers)}`)
|
||||
let cert = socket.getPeerCertificate(true)
|
||||
let certPEM = ''
|
||||
let fingerprints = {}
|
||||
while (cert != null && fingerprints[cert.fingerprint] != '1') {
|
||||
fingerprints[cert.fingerprint] = '1'
|
||||
certPEM = certPEM + '-----BEGIN CERTIFICATE-----\n'
|
||||
let certEncoded = cert.raw.toString('base64')
|
||||
for (let i = 0; i < certEncoded.length; i++) {
|
||||
certPEM = certPEM + certEncoded[i]
|
||||
if (i != certEncoded.length - 1 && (i + 1) % 64 == 0) {
|
||||
certPEM = certPEM + '\n'
|
||||
}
|
||||
}
|
||||
certPEM = certPEM + '\n-----END CERTIFICATE-----\n'
|
||||
cert = cert.issuerCertificate
|
||||
}
|
||||
console.log(certPEM)
|
||||
fs.writeFileSync('./download_ca_cert.pem', certPEM)
|
||||
res.on('data', d => {
|
||||
process.stdout.write(d)
|
||||
})
|
||||
})
|
||||
req.on('error', error => {
|
||||
console.error(error)
|
||||
})
|
||||
req.end()
|
||||
}
|
||||
else {
|
||||
const auth = 'Basic ' + Buffer.from(proxyUsername + ':' + proxyPassword).toString('base64')
|
||||
|
||||
const options = {
|
||||
host: proxyHost,
|
||||
port: proxyPort,
|
||||
method: 'CONNECT',
|
||||
path: `${hostname}:${port}`,
|
||||
}
|
||||
|
||||
if (proxyUsername != '' || proxyPassword != '') {
|
||||
options.headers = {
|
||||
'Proxy-Authorization': auth,
|
||||
}
|
||||
}
|
||||
|
||||
http.request(options).on('connect', (res, socket) => {
|
||||
if (res.statusCode != 200) {
|
||||
throw new Error(`Proxy returns code: ${res.statusCode}`)
|
||||
}
|
||||
|
||||
https.get({
|
||||
host: hostname,
|
||||
port: port,
|
||||
socket: socket,
|
||||
agent: false,
|
||||
path: '/',
|
||||
headers: {
|
||||
'User-Agent': 'GitHubActionsRunnerCheck/1.0',
|
||||
'Authorization': `token ${pat}`
|
||||
}
|
||||
}, (res) => {
|
||||
let cert = res.socket.getPeerCertificate(true)
|
||||
let certPEM = ''
|
||||
let fingerprints = {}
|
||||
while (cert != null && fingerprints[cert.fingerprint] != '1') {
|
||||
fingerprints[cert.fingerprint] = '1'
|
||||
certPEM = certPEM + '-----BEGIN CERTIFICATE-----\n'
|
||||
let certEncoded = cert.raw.toString('base64')
|
||||
for (let i = 0; i < certEncoded.length; i++) {
|
||||
certPEM = certPEM + certEncoded[i]
|
||||
if (i != certEncoded.length - 1 && (i + 1) % 64 == 0) {
|
||||
certPEM = certPEM + '\n'
|
||||
}
|
||||
}
|
||||
certPEM = certPEM + '\n-----END CERTIFICATE-----\n'
|
||||
cert = cert.issuerCertificate
|
||||
}
|
||||
console.log(certPEM)
|
||||
fs.writeFileSync('./download_ca_cert.pem', certPEM)
|
||||
console.log(`statusCode: ${res.statusCode}`)
|
||||
console.log(`headers: ${JSON.stringify(res.headers)}`)
|
||||
res.on('data', d => {
|
||||
process.stdout.write(d)
|
||||
})
|
||||
})
|
||||
}).on('error', (err) => {
|
||||
console.error('error', err)
|
||||
}).end()
|
||||
}
|
||||
75
src/Misc/layoutbin/checkScripts/makeWebRequest.js
Normal file
75
src/Misc/layoutbin/checkScripts/makeWebRequest.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const https = require('https')
|
||||
const http = require('http')
|
||||
const hostname = process.env['HOSTNAME'] || ''
|
||||
const port = process.env['PORT'] || ''
|
||||
const path = process.env['PATH'] || ''
|
||||
const pat = process.env['PAT'] || ''
|
||||
const proxyHost = process.env['PROXYHOST'] || ''
|
||||
const proxyPort = process.env['PROXYPORT'] || ''
|
||||
const proxyUsername = process.env['PROXYUSERNAME'] || ''
|
||||
const proxyPassword = process.env['PROXYPASSWORD'] || ''
|
||||
|
||||
if (proxyHost === '') {
|
||||
const options = {
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
path: path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'GitHubActionsRunnerCheck/1.0',
|
||||
'Authorization': `token ${pat}`,
|
||||
}
|
||||
}
|
||||
const req = https.request(options, res => {
|
||||
console.log(`statusCode: ${res.statusCode}`)
|
||||
console.log(`headers: ${JSON.stringify(res.headers)}`)
|
||||
|
||||
res.on('data', d => {
|
||||
process.stdout.write(d)
|
||||
})
|
||||
})
|
||||
req.on('error', error => {
|
||||
console.error(error)
|
||||
})
|
||||
req.end()
|
||||
}
|
||||
else {
|
||||
const proxyAuth = 'Basic ' + Buffer.from(proxyUsername + ':' + proxyPassword).toString('base64')
|
||||
const options = {
|
||||
hostname: proxyHost,
|
||||
port: proxyPort,
|
||||
method: 'CONNECT',
|
||||
path: `${hostname}:${port}`
|
||||
}
|
||||
|
||||
if (proxyUsername != '' || proxyPassword != '') {
|
||||
options.headers = {
|
||||
'Proxy-Authorization': proxyAuth,
|
||||
}
|
||||
}
|
||||
http.request(options).on('connect', (res, socket) => {
|
||||
if (res.statusCode != 200) {
|
||||
throw new Error(`Proxy returns code: ${res.statusCode}`)
|
||||
}
|
||||
https.get({
|
||||
host: hostname,
|
||||
port: port,
|
||||
socket: socket,
|
||||
agent: false,
|
||||
path: path,
|
||||
headers: {
|
||||
'User-Agent': 'GitHubActionsRunnerCheck/1.0',
|
||||
'Authorization': `token ${pat}`,
|
||||
}
|
||||
}, (res) => {
|
||||
console.log(`statusCode: ${res.statusCode}`)
|
||||
console.log(`headers: ${JSON.stringify(res.headers)}`)
|
||||
|
||||
res.on('data', d => {
|
||||
process.stdout.write(d)
|
||||
})
|
||||
})
|
||||
}).on('error', (err) => {
|
||||
console.error('error', err)
|
||||
}).end()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
SVC_NAME="{{SvcNameVar}}"
|
||||
SVC_NAME=${SVC_NAME// /_}
|
||||
SVC_DESCRIPTION="{{SvcDescription}}"
|
||||
|
||||
user_id=`id -u`
|
||||
|
||||
@@ -9,7 +9,7 @@ fi
|
||||
|
||||
# Determine OS type
|
||||
# Debian based OS (Debian, Ubuntu, Linux Mint) has /etc/debian_version
|
||||
# Fedora based OS (Fedora, Redhat, Centos, Oracle Linux 7) has /etc/redhat-release
|
||||
# Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7) has /etc/redhat-release
|
||||
# SUSE based OS (OpenSUSE, SUSE Enterprise) has ID_LIKE=suse in /etc/os-release
|
||||
|
||||
function print_errormessage()
|
||||
@@ -49,79 +49,75 @@ then
|
||||
cat /etc/debian_version
|
||||
echo "------------------------------"
|
||||
|
||||
# prefer apt over apt-get
|
||||
command -v apt
|
||||
# prefer apt-get over apt
|
||||
command -v apt-get
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
apt update && apt install -y liblttng-ust0 libkrb5-3 zlib1g
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'apt' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# libissl version prefer: libssl1.1 -> libssl1.0.2 -> libssl1.0.0
|
||||
apt install -y libssl1.1$ || apt install -y libssl1.0.2$ || apt install -y libssl1.0.0$
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'apt' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# libicu version prefer: libicu63 -> libicu60 -> libicu57 -> libicu55 -> libicu52
|
||||
apt install -y libicu63 || apt install -y libicu60 || apt install -y libicu57 || apt install -y libicu55 || apt install -y libicu52
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'apt' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
apt_get=apt-get
|
||||
else
|
||||
command -v apt-get
|
||||
command -v apt
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
apt-get update && apt-get install -y liblttng-ust0 libkrb5-3 zlib1g
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'apt-get' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# libissl version prefer: libssl1.1 -> libssl1.0.2 -> libssl1.0.0
|
||||
apt-get install -y libssl1.1$ || apt-get install -y libssl1.0.2$ || apt install -y libssl1.0.0$
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'apt-get' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# libicu version prefer: libicu63 -> libicu60 -> libicu57 -> libicu55 -> libicu52
|
||||
apt-get install -y libicu63 || apt-get install -y libicu60 || apt install -y libicu57 || apt install -y libicu55 || apt install -y libicu52
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'apt-get' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
apt_get=apt
|
||||
else
|
||||
echo "Can not find 'apt' or 'apt-get'"
|
||||
echo "Found neither 'apt-get' nor 'apt'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
$apt_get update && $apt_get install -y liblttng-ust0 libkrb5-3 zlib1g
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'$apt_get' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
apt_get_with_fallbacks() {
|
||||
$apt_get install -y $1
|
||||
fail=$?
|
||||
if [ $fail -eq 0 ]
|
||||
then
|
||||
if [ "${1#"${1%?}"}" = '$' ]; then
|
||||
dpkg -l "${1%?}" > /dev/null 2> /dev/null
|
||||
fail=$?
|
||||
fi
|
||||
fi
|
||||
if [ $fail -ne 0 ]
|
||||
then
|
||||
shift
|
||||
if [ -n "$1" ]
|
||||
then
|
||||
apt_get_with_fallbacks "$@"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
apt_get_with_fallbacks libssl1.1$ libssl1.0.2$ libssl1.0.0$
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'$apt_get' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
apt_get_with_fallbacks libicu72 libicu71 libicu70 libicu69 libicu68 libicu67 libicu66 libicu65 libicu63 libicu60 libicu57 libicu55 libicu52
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "'$apt_get' failed with exit code '$?'"
|
||||
print_errormessage
|
||||
exit 1
|
||||
fi
|
||||
elif [ -e /etc/redhat-release ]
|
||||
then
|
||||
echo "The current OS is Fedora based"
|
||||
echo "--------Redhat Version--------"
|
||||
echo "--Fedora/RHEL/CentOS Version--"
|
||||
cat /etc/redhat-release
|
||||
echo "------------------------------"
|
||||
|
||||
# use dnf on fedora
|
||||
# use yum on centos and redhat
|
||||
# use yum on centos and rhel
|
||||
if [ -e /etc/fedora-release ]
|
||||
then
|
||||
command -v dnf
|
||||
@@ -191,7 +187,7 @@ then
|
||||
redhatRelease=$(</etc/redhat-release)
|
||||
if [[ $redhatRelease == "CentOS release 6."* || $redhatRelease == "Red Hat Enterprise Linux Server release 6."* ]]
|
||||
then
|
||||
echo "The current OS is Red Hat Enterprise Linux 6 or Centos 6"
|
||||
echo "The current OS is Red Hat Enterprise Linux 6 or CentOS 6"
|
||||
|
||||
# Install known dependencies, as a best effort.
|
||||
# The remaining dependencies are covered by the GitHub doc that will be shown by `print_rhel6message`
|
||||
|
||||
13
src/Misc/layoutbin/macos-run-invoker.js
Normal file
13
src/Misc/layoutbin/macos-run-invoker.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const { spawn } = require('child_process');
|
||||
// argv[0] = node
|
||||
// argv[1] = macos-run-invoker.js
|
||||
var shell = process.argv[2];
|
||||
var args = process.argv.slice(3);
|
||||
console.log(`::debug::macos-run-invoker: ${shell}`);
|
||||
console.log(`::debug::macos-run-invoker: ${JSON.stringify(args)}`);
|
||||
var launch = spawn(shell, args, { stdio: 'inherit' });
|
||||
launch.on('exit', function (code) {
|
||||
if (code !== 0) {
|
||||
process.exit(code);
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
SVC_NAME="{{SvcNameVar}}"
|
||||
SVC_NAME=${SVC_NAME// /_}
|
||||
SVC_DESCRIPTION="{{SvcDescription}}"
|
||||
|
||||
SVC_CMD=$1
|
||||
@@ -62,12 +63,25 @@ function install()
|
||||
|
||||
sed "s/{{User}}/${run_as_user}/g; s/{{Description}}/$(echo ${SVC_DESCRIPTION} | sed -e 's/[\/&]/\\&/g')/g; s/{{RunnerRoot}}/$(echo ${RUNNER_ROOT} | sed -e 's/[\/&]/\\&/g')/g;" "${TEMPLATE_PATH}" > "${TEMP_PATH}" || failed "failed to create replacement temp file"
|
||||
mv "${TEMP_PATH}" "${UNIT_PATH}" || failed "failed to copy unit file"
|
||||
|
||||
# Recent Fedora based Linux (CentOS/Redhat) has SELinux enabled by default
|
||||
# We need to restore security context on the unit file we added otherwise SystemD have no access to it.
|
||||
command -v getenforce > /dev/null
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
selinuxEnabled=$(getenforce)
|
||||
if [[ $selinuxEnabled == "Enforcing" ]]
|
||||
then
|
||||
# SELinux is enabled, we will need to Restore SELinux Context for the service file
|
||||
restorecon -r -v "${UNIT_PATH}" || failed "failed to restore SELinux context on ${UNIT_PATH}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# unit file should not be executable and world writable
|
||||
chmod 664 ${UNIT_PATH} || failed "failed to set permissions on ${UNIT_PATH}"
|
||||
chmod 664 "${UNIT_PATH}" || failed "failed to set permissions on ${UNIT_PATH}"
|
||||
systemctl daemon-reload || failed "failed to reload daemons"
|
||||
|
||||
# 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.
|
||||
cp ./bin/runsvc.sh ./runsvc.sh || failed "failed to copy runsvc.sh"
|
||||
chown ${run_as_uid}:${run_as_gid} ./runsvc.sh || failed "failed to set owner for runsvc.sh"
|
||||
chmod 755 ./runsvc.sh || failed "failed to set permission for runsvc.sh"
|
||||
@@ -92,25 +106,37 @@ function stop()
|
||||
|
||||
function uninstall()
|
||||
{
|
||||
stop
|
||||
systemctl disable ${SVC_NAME} || failed "failed to disable ${SVC_NAME}"
|
||||
rm "${UNIT_PATH}" || failed "failed to delete ${UNIT_PATH}"
|
||||
if service_exists; then
|
||||
stop
|
||||
systemctl disable ${SVC_NAME} || failed "failed to disable ${SVC_NAME}"
|
||||
rm "${UNIT_PATH}" || failed "failed to delete ${UNIT_PATH}"
|
||||
else
|
||||
echo "Service ${SVC_NAME} is not installed"
|
||||
fi
|
||||
if [ -f "${CONFIG_PATH}" ]; then
|
||||
rm "${CONFIG_PATH}" || failed "failed to delete ${CONFIG_PATH}"
|
||||
fi
|
||||
systemctl daemon-reload || failed "failed to reload daemons"
|
||||
}
|
||||
|
||||
function service_exists() {
|
||||
if [ -f "${UNIT_PATH}" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function status()
|
||||
{
|
||||
if [ -f "${UNIT_PATH}" ]; then
|
||||
if service_exists; then
|
||||
echo
|
||||
echo "${UNIT_PATH}"
|
||||
else
|
||||
echo
|
||||
echo "not installed"
|
||||
echo
|
||||
return
|
||||
exit 1
|
||||
fi
|
||||
|
||||
systemctl --no-pager status ${SVC_NAME}
|
||||
|
||||
58
src/Misc/layoutbin/update.sh.template
Normal file → Executable file
58
src/Misc/layoutbin/update.sh.template
Normal file → Executable file
@@ -18,6 +18,8 @@ downloadrunnerversion=_DOWNLOAD_RUNNER_VERSION_
|
||||
logfile="_UPDATE_LOG_"
|
||||
restartinteractiverunner=_RESTART_INTERACTIVE_RUNNER_
|
||||
|
||||
telemetryfile="$rootfolder/_diag/.telemetry"
|
||||
|
||||
# log user who run the script
|
||||
date "+[%F %T-%4N] --------whoami--------" >> "$logfile" 2>&1
|
||||
whoami >> "$logfile" 2>&1
|
||||
@@ -28,13 +30,13 @@ date "+[%F %T-%4N] Waiting for $runnerprocessname ($runnerpid) to complete" >> "
|
||||
while [ -e /proc/$runnerpid ]
|
||||
do
|
||||
date "+[%F %T-%4N] Process $runnerpid still running" >> "$logfile" 2>&1
|
||||
ping -c 2 127.0.0.1 >nul
|
||||
sleep 2
|
||||
done
|
||||
date "+[%F %T-%4N] Process $runnerpid finished running" >> "$logfile" 2>&1
|
||||
|
||||
# start re-organize folders
|
||||
date "+[%F %T-%4N] Sleep 1 more second to make sure process exited" >> "$logfile" 2>&1
|
||||
ping -c 2 127.0.0.1 >nul
|
||||
sleep 1
|
||||
|
||||
# the folder structure under runner root will be
|
||||
# ./bin -> bin.2.100.0 (junction folder)
|
||||
@@ -118,6 +120,58 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# fix upgrade issue with macOS when running as a service
|
||||
attemptedtargetedfix=0
|
||||
currentplatform=$(uname | awk '{print tolower($0)}')
|
||||
if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
||||
# 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
|
||||
# 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
|
||||
# assumption here is only one process is invoking rootfolder/runsvc.sh
|
||||
procgroup=$(ps x -o pgid,command | grep "$rootfolder/runsvc.sh" | grep -v grep | awk '{print $1}')
|
||||
if [[ $? -eq 0 && -n "$procgroup" ]]
|
||||
then
|
||||
# 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
|
||||
path=$(lsof -a -g "$procgroup" -F n | grep node12/bin/node | grep externals | tail -1 | cut -c2-)
|
||||
if [[ $? -eq 0 && -n "$path" ]]
|
||||
then
|
||||
# trim the last 5 characters of the path '/node'
|
||||
trimmedpath=$(dirname "$path")
|
||||
if [[ $? -eq 0 && -n "$trimmedpath" ]]
|
||||
then
|
||||
attemptedtargetedfix=1
|
||||
# Create the path if it does not exist
|
||||
if [[ ! -e "$path" ]]
|
||||
then
|
||||
date "+[%F %T-%4N] Creating fallback node at path $path" >> "$logfile" 2>&1
|
||||
mkdir -p "$trimmedpath"
|
||||
cp "$rootfolder/externals/node12/bin/node" "$path"
|
||||
else
|
||||
date "+[%F %T-%4N] Path for fallback node exists, skipping creating $path" >> "$logfile" 2>&1
|
||||
fi
|
||||
else
|
||||
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to trim runner path. TrimmedPath: $trimmedpath, path: $path, pgid: $procgroup, root: $rootfolder" >> "$logfile" 2>&1
|
||||
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to trim runner path. TrimmedPath: $trimmedpath, path: $path, pgid: $procgroup, root: $rootfolder" >> "$telemetryfile" 2>&1
|
||||
fi
|
||||
else
|
||||
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to find runner path. Path: $path, pgid: $procgroup, root: $rootfolder" >> "$logfile" 2>&1
|
||||
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to find runner path. Path: $path, pgid: $procgroup, root: $rootfolder" >> "$telemetryfile" 2>&1
|
||||
fi
|
||||
else
|
||||
runproc=$(ps x -o pgid,command | grep "run.sh" | grep -v grep | awk '{print $1}')
|
||||
if [[ $? -eq 0 && -n "$runproc" ]]
|
||||
then
|
||||
date "+[%F %T-%4N] Running as ephemeral using run.sh, no need to recreate node folder" >> "$logfile" 2>&1
|
||||
else
|
||||
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to find runner pgid. pgid: $procgroup, root: $rootfolder" >> "$logfile" 2>&1
|
||||
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to find runner pgid. pgid: $procgroup, root: $rootfolder" >> "$telemetryfile" 2>&1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
date "+[%F %T-%4N] Update succeed" >> "$logfile"
|
||||
|
||||
# rename the update log file with %logfile%.succeed/.failed/succeedneedrestart
|
||||
|
||||
@@ -18,24 +18,26 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
message="Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 3.0 dependencies."
|
||||
|
||||
ldd ./bin/libcoreclr.so | grep 'not found'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Dependencies is missing for Dotnet Core 3.0"
|
||||
echo "Execute ./bin/installdependencies.sh to install any missing Dotnet Core 3.0 dependencies."
|
||||
echo $message
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ldd ./bin/System.Security.Cryptography.Native.OpenSsl.so | grep 'not found'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Dependencies is missing for Dotnet Core 3.0"
|
||||
echo "Execute ./bin/installdependencies.sh to install any missing Dotnet Core 3.0 dependencies."
|
||||
echo $message
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ldd ./bin/System.IO.Compression.Native.so | grep 'not found'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Dependencies is missing for Dotnet Core 3.0"
|
||||
echo "Execute ./bin/installdependencies.sh to install any missing Dotnet Core 3.0 dependencies."
|
||||
echo $message
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -50,10 +52,10 @@ then
|
||||
fi
|
||||
|
||||
libpath=${LD_LIBRARY_PATH:-}
|
||||
$LDCONFIG_COMMAND -NXv ${libpath//:/} 2>&1 | grep libicu >/dev/null 2>&1
|
||||
$LDCONFIG_COMMAND -NXv ${libpath//:/ } 2>&1 | grep libicu >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Libicu's dependencies is missing for Dotnet Core 3.0"
|
||||
echo "Execute ./bin/installdependencies.sh to install any missing Dotnet Core 3.0 dependencies."
|
||||
echo $message
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -67,7 +69,7 @@ while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symli
|
||||
[[ $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 )"
|
||||
cd $DIR
|
||||
cd "$DIR"
|
||||
|
||||
source ./env.sh
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ varCheckList=(
|
||||
'ANT_HOME'
|
||||
'M2_HOME'
|
||||
'ANDROID_HOME'
|
||||
'ANDROID_SDK_ROOT'
|
||||
'GRADLE_HOME'
|
||||
'NVM_BIN'
|
||||
'NVM_PATH'
|
||||
|
||||
@@ -26,24 +26,37 @@ if [[ "$1" == "localRun" ]]; then
|
||||
else
|
||||
"$DIR"/bin/Runner.Listener run $*
|
||||
|
||||
# Return code 4 means the run once runner received an update message.
|
||||
# Sleep 5 seconds to wait for the update process finish and run the runner again.
|
||||
# Return code 3 means the run once runner received an update message.
|
||||
# Sleep 5 seconds to wait for the update process finish
|
||||
returnCode=$?
|
||||
if [[ $returnCode == 4 ]]; then
|
||||
if [[ $returnCode == 3 ]]; then
|
||||
if [ ! -x "$(command -v sleep)" ]; then
|
||||
if [ ! -x "$(command -v ping)" ]; then
|
||||
COUNT="0"
|
||||
while [[ $COUNT != 5000 ]]; do
|
||||
echo "SLEEP" >nul
|
||||
echo "SLEEP" > /dev/null
|
||||
COUNT=$[$COUNT+1]
|
||||
done
|
||||
else
|
||||
ping -n 5 127.0.0.1 >nul
|
||||
ping -c 5 127.0.0.1 > /dev/null
|
||||
fi
|
||||
else
|
||||
sleep 5 >nul
|
||||
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
|
||||
exit $returnCode
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace GitHub.Runner.Common
|
||||
[DataContract]
|
||||
public sealed class RunnerSettings
|
||||
{
|
||||
[DataMember(Name = "IsHostedServer", EmitDefaultValue = false)]
|
||||
private bool? _isHostedServer;
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public int AgentId { get; set; }
|
||||
|
||||
@@ -30,6 +33,9 @@ namespace GitHub.Runner.Common
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string PoolName { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public bool Ephemeral { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string ServerUrl { get; set; }
|
||||
|
||||
@@ -42,6 +48,21 @@ namespace GitHub.Runner.Common
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string MonitorSocketAddress { get; set; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public bool IsHostedServer
|
||||
{
|
||||
get
|
||||
{
|
||||
// Old runners do not have this property. Hosted runners likely don't have this property either.
|
||||
return _isHostedServer ?? true;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_isHostedServer = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
// Computed property for convenience. Can either return:
|
||||
// 1. If runner was configured at the repo level, returns something like: "myorg/myrepo"
|
||||
@@ -69,6 +90,15 @@ namespace GitHub.Runner.Common
|
||||
return repoOrOrgName;
|
||||
}
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (_isHostedServer.HasValue && _isHostedServer.Value)
|
||||
{
|
||||
_isHostedServer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ServiceLocator(Default = typeof(ConfigurationStore))]
|
||||
@@ -81,9 +111,9 @@ namespace GitHub.Runner.Common
|
||||
CredentialData GetMigratedCredentials();
|
||||
RunnerSettings GetSettings();
|
||||
void SaveCredential(CredentialData credential);
|
||||
void SaveMigratedCredential(CredentialData credential);
|
||||
void SaveSettings(RunnerSettings settings);
|
||||
void DeleteCredential();
|
||||
void DeleteMigratedCredential();
|
||||
void DeleteSettings();
|
||||
}
|
||||
|
||||
@@ -205,21 +235,6 @@ namespace GitHub.Runner.Common
|
||||
File.SetAttributes(_credFilePath, File.GetAttributes(_credFilePath) | FileAttributes.Hidden);
|
||||
}
|
||||
|
||||
public void SaveMigratedCredential(CredentialData credential)
|
||||
{
|
||||
Trace.Info("Saving {0} migrated credential @ {1}", credential.Scheme, _migratedCredFilePath);
|
||||
if (File.Exists(_migratedCredFilePath))
|
||||
{
|
||||
// Delete existing credential file first, since the file is hidden and not able to overwrite.
|
||||
Trace.Info("Delete exist runner migrated credential file.");
|
||||
IOUtil.DeleteFile(_migratedCredFilePath);
|
||||
}
|
||||
|
||||
IOUtil.SaveObject(credential, _migratedCredFilePath);
|
||||
Trace.Info("Migrated Credentials Saved.");
|
||||
File.SetAttributes(_migratedCredFilePath, File.GetAttributes(_migratedCredFilePath) | FileAttributes.Hidden);
|
||||
}
|
||||
|
||||
public void SaveSettings(RunnerSettings settings)
|
||||
{
|
||||
Trace.Info("Saving runner settings.");
|
||||
@@ -241,6 +256,11 @@ namespace GitHub.Runner.Common
|
||||
IOUtil.Delete(_migratedCredFilePath, default(CancellationToken));
|
||||
}
|
||||
|
||||
public void DeleteMigratedCredential()
|
||||
{
|
||||
IOUtil.Delete(_migratedCredFilePath, default(CancellationToken));
|
||||
}
|
||||
|
||||
public void DeleteSettings()
|
||||
{
|
||||
IOUtil.Delete(_configFilePath, default(CancellationToken));
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace GitHub.Runner.Common
|
||||
Certificates,
|
||||
Options,
|
||||
SetupInfo,
|
||||
Telemetry
|
||||
}
|
||||
|
||||
public static class Constants
|
||||
@@ -41,6 +42,8 @@ namespace GitHub.Runner.Common
|
||||
public static string PluginTracePrefix = "##[plugin.trace]";
|
||||
public static readonly int RunnerDownloadRetryMaxAttempts = 3;
|
||||
|
||||
public static readonly int CompositeActionsMaxDepth = 9;
|
||||
|
||||
// This enum is embedded within the Constants class to make it easier to reference and avoid
|
||||
// ambiguous type reference with System.Runtime.InteropServices.OSPlatform and System.Runtime.InteropServices.Architecture
|
||||
public enum OSPlatform
|
||||
@@ -87,9 +90,10 @@ namespace GitHub.Runner.Common
|
||||
public static class Args
|
||||
{
|
||||
public static readonly string Auth = "auth";
|
||||
public static readonly string Labels = "labels";
|
||||
public static readonly string MonitorSocketAddress = "monitorsocketaddress";
|
||||
public static readonly string Name = "name";
|
||||
public static readonly string Pool = "pool";
|
||||
public static readonly string RunnerGroup = "runnergroup";
|
||||
public static readonly string StartupType = "startuptype";
|
||||
public static readonly string Url = "url";
|
||||
public static readonly string UserName = "username";
|
||||
@@ -98,9 +102,11 @@ namespace GitHub.Runner.Common
|
||||
|
||||
// Secret args. Must be added to the "Secrets" getter as well.
|
||||
public static readonly string Token = "token";
|
||||
public static readonly string PAT = "pat";
|
||||
public static readonly string WindowsLogonPassword = "windowslogonpassword";
|
||||
public static string[] Secrets => new[]
|
||||
{
|
||||
PAT,
|
||||
Token,
|
||||
WindowsLogonPassword,
|
||||
};
|
||||
@@ -118,10 +124,12 @@ namespace GitHub.Runner.Common
|
||||
//validFlags array as well present in the CommandSettings.cs
|
||||
public static class Flags
|
||||
{
|
||||
public static readonly string Check = "check";
|
||||
public static readonly string Commit = "commit";
|
||||
public static readonly string Ephemeral = "ephemeral";
|
||||
public static readonly string Help = "help";
|
||||
public static readonly string Replace = "replace";
|
||||
public static readonly string Once = "once";
|
||||
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 Unattended = "unattended";
|
||||
public static readonly string Version = "version";
|
||||
@@ -136,6 +144,18 @@ namespace GitHub.Runner.Common
|
||||
public const int RunnerUpdating = 3;
|
||||
public const int RunOnceRunnerUpdating = 4;
|
||||
}
|
||||
|
||||
public static class Features
|
||||
{
|
||||
public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
|
||||
}
|
||||
|
||||
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
||||
public static readonly string WorkerCrash = "WORKER_CRASH";
|
||||
public static readonly string LowDiskSpace = "LOW_DISK_SPACE";
|
||||
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 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 class RunnerEvent
|
||||
@@ -194,6 +214,8 @@ namespace GitHub.Runner.Common
|
||||
//
|
||||
// Keep alphabetical
|
||||
//
|
||||
public static readonly string AllowUnsupportedCommands = "ACTIONS_ALLOW_UNSECURE_COMMANDS";
|
||||
public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS";
|
||||
public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG";
|
||||
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
|
||||
}
|
||||
|
||||
@@ -51,11 +51,22 @@ namespace GitHub.Runner.Common
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.RemoveMatcherCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.WarningCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.ErrorCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.NoticeCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.DebugCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.GroupCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.EndGroupCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.EchoCommandExtension, Runner.Worker");
|
||||
break;
|
||||
case "GitHub.Runner.Worker.IFileCommandExtension":
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.AddPathFileCommand, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.SetEnvFileCommand, Runner.Worker");
|
||||
break;
|
||||
case "GitHub.Runner.Listener.Check.ICheckExtension":
|
||||
Add<T>(extensions, "GitHub.Runner.Listener.Check.InternetCheck, Runner.Listener");
|
||||
Add<T>(extensions, "GitHub.Runner.Listener.Check.ActionsCheck, Runner.Listener");
|
||||
Add<T>(extensions, "GitHub.Runner.Listener.Check.GitCheck, Runner.Listener");
|
||||
Add<T>(extensions, "GitHub.Runner.Listener.Check.NodeJsCheck, Runner.Listener");
|
||||
break;
|
||||
default:
|
||||
// This should never happen.
|
||||
throw new NotSupportedException($"Unexpected extension type: '{typeof(T).FullName}'");
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
using GitHub.Runner.Common.Util;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Diagnostics.Tracing;
|
||||
using GitHub.DistributedTask.Logging;
|
||||
using System.Net.Http.Headers;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
@@ -24,10 +24,10 @@ namespace GitHub.Runner.Common
|
||||
CancellationToken RunnerShutdownToken { get; }
|
||||
ShutdownReason RunnerShutdownReason { get; }
|
||||
ISecretMasker SecretMasker { get; }
|
||||
ProductInfoHeaderValue UserAgent { get; }
|
||||
List<ProductInfoHeaderValue> UserAgents { get; }
|
||||
RunnerWebProxy WebProxy { get; }
|
||||
string GetDirectory(WellKnownDirectory directory);
|
||||
string GetConfigFile(WellKnownConfigFile configFile);
|
||||
string GetDirectory(WellKnownDirectory directory, string rootCaller = "", [CallerMemberName] string caller = "");
|
||||
string GetConfigFile(WellKnownConfigFile configFile, string rootCaller = "", [CallerMemberName] string caller = "");
|
||||
Tracing GetTrace(string name);
|
||||
Task Delay(TimeSpan delay, CancellationToken cancellationToken);
|
||||
T CreateService<T>() where T : class, IRunnerService;
|
||||
@@ -54,7 +54,7 @@ namespace GitHub.Runner.Common
|
||||
private readonly ConcurrentDictionary<Type, object> _serviceInstances = new ConcurrentDictionary<Type, object>();
|
||||
private readonly ConcurrentDictionary<Type, Type> _serviceTypes = new ConcurrentDictionary<Type, Type>();
|
||||
private readonly ISecretMasker _secretMasker = new SecretMasker();
|
||||
private readonly ProductInfoHeaderValue _userAgent = new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version);
|
||||
private readonly List<ProductInfoHeaderValue> _userAgents = new List<ProductInfoHeaderValue>() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
|
||||
private CancellationTokenSource _runnerShutdownTokenSource = new CancellationTokenSource();
|
||||
private object _perfLock = new object();
|
||||
private Tracing _trace;
|
||||
@@ -72,7 +72,7 @@ namespace GitHub.Runner.Common
|
||||
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
|
||||
public ShutdownReason RunnerShutdownReason { get; private set; }
|
||||
public ISecretMasker SecretMasker => _secretMasker;
|
||||
public ProductInfoHeaderValue UserAgent => _userAgent;
|
||||
public List<ProductInfoHeaderValue> UserAgents => _userAgents;
|
||||
public RunnerWebProxy WebProxy => _webProxy;
|
||||
public HostContext(string hostType, string logFile = null)
|
||||
{
|
||||
@@ -85,10 +85,14 @@ namespace GitHub.Runner.Common
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscape);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift1);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift2);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.CommandLineArgumentEscape);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.ExpressionStringEscape);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.XmlDataEscape);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.TrimDoubleQuotes);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPreAmpersandEscape);
|
||||
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPostAmpersandEscape);
|
||||
|
||||
// Create the trace manager.
|
||||
if (string.IsNullOrEmpty(logFile))
|
||||
@@ -189,10 +193,34 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
_trace.Info($"No proxy settings were found based on environmental variables (http_proxy/https_proxy/HTTP_PROXY/HTTPS_PROXY)");
|
||||
}
|
||||
|
||||
var credFile = GetConfigFile(WellKnownConfigFile.Credentials);
|
||||
if (File.Exists(credFile))
|
||||
{
|
||||
var credData = IOUtil.LoadObject<CredentialData>(credFile);
|
||||
if (credData != null &&
|
||||
credData.Data.TryGetValue("clientId", out var clientId))
|
||||
{
|
||||
_userAgents.Add(new ProductInfoHeaderValue("ClientId", clientId));
|
||||
}
|
||||
}
|
||||
|
||||
var runnerFile = GetConfigFile(WellKnownConfigFile.Runner);
|
||||
if (File.Exists(runnerFile))
|
||||
{
|
||||
var runnerSettings = IOUtil.LoadObject<RunnerSettings>(runnerFile);
|
||||
_userAgents.Add(new ProductInfoHeaderValue("RunnerId", runnerSettings.AgentId.ToString(CultureInfo.InvariantCulture)));
|
||||
_userAgents.Add(new ProductInfoHeaderValue("GroupId", runnerSettings.PoolId.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDirectory(WellKnownDirectory directory)
|
||||
public string GetDirectory(WellKnownDirectory directory, string rootCaller = "", [CallerMemberName] string caller = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(rootCaller))
|
||||
{
|
||||
rootCaller = caller;
|
||||
}
|
||||
|
||||
string path;
|
||||
switch (directory)
|
||||
{
|
||||
@@ -202,29 +230,29 @@ namespace GitHub.Runner.Common
|
||||
|
||||
case WellKnownDirectory.Diag:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
Constants.Path.DiagDirectory);
|
||||
break;
|
||||
|
||||
case WellKnownDirectory.Externals:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
Constants.Path.ExternalsDirectory);
|
||||
break;
|
||||
|
||||
case WellKnownDirectory.Root:
|
||||
path = new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName;
|
||||
path = new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin, rootCaller)).Parent.FullName;
|
||||
break;
|
||||
|
||||
case WellKnownDirectory.Temp:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Work),
|
||||
GetDirectory(WellKnownDirectory.Work, rootCaller),
|
||||
Constants.Path.TempDirectory);
|
||||
break;
|
||||
|
||||
case WellKnownDirectory.Actions:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Work),
|
||||
GetDirectory(WellKnownDirectory.Work, rootCaller),
|
||||
Constants.Path.ActionsDirectory);
|
||||
break;
|
||||
|
||||
@@ -235,14 +263,14 @@ namespace GitHub.Runner.Common
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Work),
|
||||
GetDirectory(WellKnownDirectory.Work, rootCaller),
|
||||
Constants.Path.ToolDirectory);
|
||||
}
|
||||
break;
|
||||
|
||||
case WellKnownDirectory.Update:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Work),
|
||||
GetDirectory(WellKnownDirectory.Work, rootCaller),
|
||||
Constants.Path.UpdateDirectory);
|
||||
break;
|
||||
|
||||
@@ -252,88 +280,99 @@ namespace GitHub.Runner.Common
|
||||
ArgUtil.NotNull(settings, nameof(settings));
|
||||
ArgUtil.NotNullOrEmpty(settings.WorkFolder, nameof(settings.WorkFolder));
|
||||
path = Path.GetFullPath(Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
settings.WorkFolder));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected well known directory: '{directory}'");
|
||||
throw new NotSupportedException($"Unexpected well known directory: '{directory}' ({rootCaller})");
|
||||
}
|
||||
|
||||
_trace.Info($"Well known directory '{directory}': '{path}'");
|
||||
_trace.Info($"Well known directory '{directory}': '{path}' ({rootCaller})");
|
||||
return path;
|
||||
}
|
||||
|
||||
public string GetConfigFile(WellKnownConfigFile configFile)
|
||||
public string GetConfigFile(WellKnownConfigFile configFile, string rootCaller = "", [CallerMemberName] string caller = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(rootCaller))
|
||||
{
|
||||
rootCaller = caller;
|
||||
}
|
||||
|
||||
string path;
|
||||
switch (configFile)
|
||||
{
|
||||
case WellKnownConfigFile.Runner:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".runner");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.Credentials:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".credentials");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.MigratedCredentials:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".credentials_migrated");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.RSACredentials:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".credentials_rsaparams");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.Service:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".service");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.CredentialStore:
|
||||
#if OS_OSX
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".credential_store.keychain");
|
||||
#else
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".credential_store");
|
||||
#endif
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.Certificates:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".certificates");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.Options:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".options");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.SetupInfo:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Root),
|
||||
GetDirectory(WellKnownDirectory.Root, rootCaller),
|
||||
".setup_info");
|
||||
break;
|
||||
|
||||
case WellKnownConfigFile.Telemetry:
|
||||
path = Path.Combine(
|
||||
GetDirectory(WellKnownDirectory.Diag, rootCaller),
|
||||
".telemetry");
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected well known config file: '{configFile}'");
|
||||
throw new NotSupportedException($"Unexpected well known config file: '{configFile}' ({rootCaller})");
|
||||
}
|
||||
|
||||
_trace.Info($"Well known config file '{configFile}': '{path}'");
|
||||
_trace.Info($"Well known config file '{configFile}': '{path}' ({rootCaller})");
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -603,9 +642,8 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
public static HttpClientHandler CreateHttpClientHandler(this IHostContext context)
|
||||
{
|
||||
HttpClientHandler clientHandler = new HttpClientHandler();
|
||||
clientHandler.Proxy = context.WebProxy;
|
||||
return clientHandler;
|
||||
var handlerFactory = context.GetService<IHttpClientHandlerFactory>();
|
||||
return handlerFactory.CreateClientHandler(context.WebProxy);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
19
src/Runner.Common/HttpClientHandlerFactory.cs
Normal file
19
src/Runner.Common/HttpClientHandlerFactory.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Net.Http;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
[ServiceLocator(Default = typeof(HttpClientHandlerFactory))]
|
||||
public interface IHttpClientHandlerFactory : IRunnerService
|
||||
{
|
||||
HttpClientHandler CreateClientHandler(RunnerWebProxy webProxy);
|
||||
}
|
||||
|
||||
public class HttpClientHandlerFactory : RunnerService, IHttpClientHandlerFactory
|
||||
{
|
||||
public HttpClientHandler CreateClientHandler(RunnerWebProxy webProxy)
|
||||
{
|
||||
return new HttpClientHandler() { Proxy = webProxy };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
@@ -16,12 +19,14 @@ namespace GitHub.Runner.Common
|
||||
// logging and console
|
||||
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<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<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 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<ActionDownloadInfoCollection> ResolveActionDownloadInfoAsync(Guid scopeIdentifier, string hubName, Guid planId, ActionReferenceList actions, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class JobServer : RunnerService, IJobServer
|
||||
@@ -33,7 +38,11 @@ namespace GitHub.Runner.Common
|
||||
public async Task ConnectAsync(VssConnection jobConnection)
|
||||
{
|
||||
_connection = jobConnection;
|
||||
int attemptCount = 5;
|
||||
int totalAttempts = 5;
|
||||
int attemptCount = totalAttempts;
|
||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
||||
var runnerSettings = configurationStore.GetSettings();
|
||||
|
||||
while (!_connection.HasAuthenticated && attemptCount-- > 0)
|
||||
{
|
||||
try
|
||||
@@ -43,17 +52,71 @@ namespace GitHub.Runner.Common
|
||||
}
|
||||
catch (Exception ex) when (attemptCount > 0)
|
||||
{
|
||||
Trace.Info($"Catch exception during connect. {attemptCount} attemp left.");
|
||||
Trace.Info($"Catch exception during connect. {attemptCount} attempts left.");
|
||||
Trace.Error(ex);
|
||||
|
||||
if (runnerSettings.IsHostedServer)
|
||||
{
|
||||
await CheckNetworkEndpointsAsync(attemptCount);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(100);
|
||||
int attempt = totalAttempts - attemptCount;
|
||||
TimeSpan backoff = BackoffTimerHelper.GetExponentialBackoff(attempt, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(3.2), TimeSpan.FromMilliseconds(100));
|
||||
|
||||
await Task.Delay(backoff);
|
||||
}
|
||||
|
||||
_taskClient = _connection.GetClient<TaskHttpClient>();
|
||||
_hasConnection = true;
|
||||
}
|
||||
|
||||
private async Task CheckNetworkEndpointsAsync(int attemptsLeft)
|
||||
{
|
||||
try
|
||||
{
|
||||
Trace.Info("Requesting Actions Service health endpoint status");
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var actionsClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
var baseUri = new Uri(_connection.Uri.GetLeftPart(UriPartial.Authority));
|
||||
|
||||
actionsClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||
|
||||
// Call the _apis/health endpoint, and include how many attempts are left as a URL query for easy tracking
|
||||
var response = await actionsClient.GetAsync(new Uri(baseUri, $"_apis/health?_internalRunnerAttemptsLeft={attemptsLeft}"));
|
||||
Trace.Info($"Actions health status code: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error, but continue as this call is best-effort
|
||||
Trace.Info($"Actions Service health endpoint failed due to {ex.GetType().Name}");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Trace.Info("Requesting Github API endpoint status");
|
||||
// This is a dotcom public API... just call it directly
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var gitHubClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
gitHubClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||
|
||||
// Call the api.github.com endpoint, and include how many attempts are left as a URL query for easy tracking
|
||||
var response = await gitHubClient.GetAsync($"https://api.github.com?_internalRunnerAttemptsLeft={attemptsLeft}");
|
||||
Trace.Info($"api.github.com status code: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error, but continue as this call is best-effort
|
||||
Trace.Info($"Github API endpoint failed due to {ex.GetType().Name}");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckConnection()
|
||||
{
|
||||
if (!_hasConnection)
|
||||
@@ -78,6 +141,12 @@ namespace GitHub.Runner.Common
|
||||
return _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)
|
||||
{
|
||||
CheckConnection();
|
||||
return _taskClient.AppendTimelineRecordFeedAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, stepId, lines, startLine, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, string type, string name, Stream uploadStream, CancellationToken cancellationToken)
|
||||
{
|
||||
CheckConnection();
|
||||
@@ -113,5 +182,14 @@ namespace GitHub.Runner.Common
|
||||
CheckConnection();
|
||||
return _taskClient.GetTimelineAsync(scopeIdentifier, hubName, planId, timelineId, includeRecords: true, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
// Action download info
|
||||
//-----------------------------------------------------------------
|
||||
public Task<ActionDownloadInfoCollection> ResolveActionDownloadInfoAsync(Guid scopeIdentifier, string hubName, Guid planId, ActionReferenceList actions, CancellationToken cancellationToken)
|
||||
{
|
||||
CheckConnection();
|
||||
return _taskClient.ResolveActionDownloadInfoAsync(scopeIdentifier, hubName, planId, actions, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ namespace GitHub.Runner.Common
|
||||
[ServiceLocator(Default = typeof(JobServerQueue))]
|
||||
public interface IJobServerQueue : IRunnerService, IThrottlingReporter
|
||||
{
|
||||
TaskCompletionSource<int> JobRecordUpdated { get; }
|
||||
event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
|
||||
Task ShutdownAsync();
|
||||
void Start(Pipelines.AgentJobRequestMessage jobRequest);
|
||||
void QueueWebConsoleLine(Guid stepRecordId, string line);
|
||||
void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber = null);
|
||||
void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource);
|
||||
void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord);
|
||||
}
|
||||
@@ -62,8 +63,11 @@ namespace GitHub.Runner.Common
|
||||
private IJobServer _jobServer;
|
||||
private Task[] _allDequeueTasks;
|
||||
private readonly TaskCompletionSource<int> _jobCompletionSource = new TaskCompletionSource<int>();
|
||||
private readonly TaskCompletionSource<int> _jobRecordUpdated = new TaskCompletionSource<int>();
|
||||
private bool _queueInProcess = false;
|
||||
|
||||
public TaskCompletionSource<int> JobRecordUpdated => _jobRecordUpdated;
|
||||
|
||||
public event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
|
||||
|
||||
// Web console dequeue will start with process queue every 250ms for the first 60*4 times (~60 seconds).
|
||||
@@ -155,10 +159,10 @@ namespace GitHub.Runner.Common
|
||||
Trace.Info("All queue process tasks have been stopped, and all queues are drained.");
|
||||
}
|
||||
|
||||
public void QueueWebConsoleLine(Guid stepRecordId, string line)
|
||||
public void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber)
|
||||
{
|
||||
Trace.Verbose("Enqueue web console line queue: {0}", line);
|
||||
_webConsoleLineQueue.Enqueue(new ConsoleLineInfo(stepRecordId, line));
|
||||
_webConsoleLineQueue.Enqueue(new ConsoleLineInfo(stepRecordId, line, lineNumber));
|
||||
}
|
||||
|
||||
public void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource)
|
||||
@@ -214,7 +218,7 @@ namespace GitHub.Runner.Common
|
||||
}
|
||||
|
||||
// Group consolelines by timeline record of each step
|
||||
Dictionary<Guid, List<string>> stepsConsoleLines = new Dictionary<Guid, List<string>>();
|
||||
Dictionary<Guid, List<TimelineRecordLogLine>> stepsConsoleLines = new Dictionary<Guid, List<TimelineRecordLogLine>>();
|
||||
List<Guid> stepRecordIds = new List<Guid>(); // We need to keep lines in order
|
||||
int linesCounter = 0;
|
||||
ConsoleLineInfo lineInfo;
|
||||
@@ -222,7 +226,7 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
if (!stepsConsoleLines.ContainsKey(lineInfo.StepRecordId))
|
||||
{
|
||||
stepsConsoleLines[lineInfo.StepRecordId] = new List<string>();
|
||||
stepsConsoleLines[lineInfo.StepRecordId] = new List<TimelineRecordLogLine>();
|
||||
stepRecordIds.Add(lineInfo.StepRecordId);
|
||||
}
|
||||
|
||||
@@ -232,7 +236,7 @@ namespace GitHub.Runner.Common
|
||||
lineInfo.Line = $"{lineInfo.Line.Substring(0, 1024)}...";
|
||||
}
|
||||
|
||||
stepsConsoleLines[lineInfo.StepRecordId].Add(lineInfo.Line);
|
||||
stepsConsoleLines[lineInfo.StepRecordId].Add(new TimelineRecordLogLine(lineInfo.Line, lineInfo.LineNumber));
|
||||
linesCounter++;
|
||||
|
||||
// process at most about 500 lines of web console line during regular timer dequeue task.
|
||||
@@ -247,13 +251,13 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
// Split consolelines into batch, each batch will container at most 100 lines.
|
||||
int batchCounter = 0;
|
||||
List<List<string>> batchedLines = new List<List<string>>();
|
||||
List<List<TimelineRecordLogLine>> batchedLines = new List<List<TimelineRecordLogLine>>();
|
||||
foreach (var line in stepsConsoleLines[stepRecordId])
|
||||
{
|
||||
var currentBatch = batchedLines.ElementAtOrDefault(batchCounter);
|
||||
if (currentBatch == null)
|
||||
{
|
||||
batchedLines.Add(new List<string>());
|
||||
batchedLines.Add(new List<TimelineRecordLogLine>());
|
||||
currentBatch = batchedLines.ElementAt(batchCounter);
|
||||
}
|
||||
|
||||
@@ -275,7 +279,6 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
Trace.Info($"Skip {batchedLines.Count - 2} batches web console lines for last run");
|
||||
batchedLines = batchedLines.TakeLast(2).ToList();
|
||||
batchedLines[0].Insert(0, "...");
|
||||
}
|
||||
|
||||
int errorCount = 0;
|
||||
@@ -284,7 +287,15 @@ namespace GitHub.Runner.Common
|
||||
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, 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)
|
||||
{
|
||||
HostContext.WritePerfCounter($"WorkerJobServerQueueAppendFirstConsoleOutput_{_planId.ToString()}");
|
||||
@@ -448,6 +459,14 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
Trace.Verbose("Cleanup buffered timeline record for timeline: {0}.", update.TimelineId);
|
||||
}
|
||||
|
||||
if (!_jobRecordUpdated.Task.IsCompleted &&
|
||||
update.PendingRecords.Any(x => x.Id == _jobTimelineRecordId && x.State != null))
|
||||
{
|
||||
// We have changed the state of the job
|
||||
Trace.Info("Job timeline record has been updated for the first time.");
|
||||
_jobRecordUpdated.TrySetResult(0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -537,6 +556,11 @@ namespace GitHub.Runner.Common
|
||||
timelineRecord.WarningCount = rec.WarningCount;
|
||||
}
|
||||
|
||||
if (rec.NoticeCount != null && rec.NoticeCount > 0)
|
||||
{
|
||||
timelineRecord.NoticeCount = rec.NoticeCount;
|
||||
}
|
||||
|
||||
if (rec.Issues.Count > 0)
|
||||
{
|
||||
timelineRecord.Issues.Clear();
|
||||
@@ -653,13 +677,15 @@ namespace GitHub.Runner.Common
|
||||
|
||||
internal class ConsoleLineInfo
|
||||
{
|
||||
public ConsoleLineInfo(Guid recordId, string line)
|
||||
public ConsoleLineInfo(Guid recordId, string line, long? lineNumber)
|
||||
{
|
||||
this.StepRecordId = recordId;
|
||||
this.Line = line;
|
||||
this.LineNumber = lineNumber;
|
||||
}
|
||||
|
||||
public Guid StepRecordId { get; set; }
|
||||
public string Line { get; set; }
|
||||
public long? LineNumber { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace GitHub.Runner.Common
|
||||
EndPage();
|
||||
_byteCount = 0;
|
||||
_dataFileName = Path.Combine(_pagesFolder, $"{_timelineId}_{_timelineRecordId}_{++_pageCount}.log");
|
||||
_pageData = new FileStream(_dataFileName, FileMode.CreateNew);
|
||||
_pageData = new FileStream(_dataFileName, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||
_pageWriter = new StreamWriter(_pageData, System.Text.Encoding.UTF8);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
@@ -68,6 +69,7 @@ namespace GitHub.Runner.Common
|
||||
|
||||
public async Task SendAsync(MessageType messageType, string body, CancellationToken cancellationToken)
|
||||
{
|
||||
Trace.Info($"Sending message of length {body.Length}, with hash '{IOUtil.GetSha256Hash(body)}'");
|
||||
await _writeStream.WriteInt32Async((int)messageType, cancellationToken);
|
||||
await _writeStream.WriteStringAsync(body, cancellationToken);
|
||||
}
|
||||
@@ -77,6 +79,7 @@ namespace GitHub.Runner.Common
|
||||
WorkerMessage result = new WorkerMessage(MessageType.NotInitialized, string.Empty);
|
||||
result.MessageType = (MessageType)await _readStream.ReadInt32Async(cancellationToken);
|
||||
result.Body = await _readStream.ReadStringAsync(cancellationToken);
|
||||
Trace.Info($"Receiving message of length {result.Body.Length}, with hash '{IOUtil.GetSha256Hash(result.Body)}'");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<AssetTargetFallback>portable-net45+win8</AssetTargetFallback>
|
||||
<NoWarn>NU1701;NU1603</NoWarn>
|
||||
<Version>$(Version)</Version>
|
||||
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
|
||||
|
||||
@@ -29,8 +29,10 @@ namespace GitHub.Runner.Common
|
||||
// Configuration
|
||||
Task<TaskAgent> AddAgentAsync(Int32 agentPoolId, TaskAgent agent);
|
||||
Task DeleteAgentAsync(int agentPoolId, int agentId);
|
||||
Task DeleteAgentAsync(int agentId);
|
||||
Task<List<TaskAgentPool>> GetAgentPoolsAsync(string agentPoolName = null, TaskAgentPoolType poolType = TaskAgentPoolType.Automation);
|
||||
Task<List<TaskAgent>> GetAgentsAsync(int agentPoolId, string agentName = null);
|
||||
Task<List<TaskAgent>> GetAgentsAsync(string agentName);
|
||||
Task<TaskAgent> ReplaceAgentAsync(int agentPoolId, TaskAgent agent);
|
||||
|
||||
// messagequeue
|
||||
@@ -41,19 +43,15 @@ namespace GitHub.Runner.Common
|
||||
|
||||
// job request
|
||||
Task<TaskAgentJobRequest> GetAgentRequestAsync(int poolId, long requestId, CancellationToken cancellationToken);
|
||||
Task<TaskAgentJobRequest> RenewAgentRequestAsync(int poolId, long requestId, Guid lockToken, CancellationToken cancellationToken);
|
||||
Task<TaskAgentJobRequest> RenewAgentRequestAsync(int poolId, long requestId, Guid lockToken, string orchestrationId, CancellationToken cancellationToken);
|
||||
Task<TaskAgentJobRequest> FinishAgentRequestAsync(int poolId, long requestId, Guid lockToken, DateTime finishTime, TaskResult result, CancellationToken cancellationToken);
|
||||
|
||||
// agent package
|
||||
Task<List<PackageMetadata>> GetPackagesAsync(string packageType, string platform, int top, CancellationToken cancellationToken);
|
||||
Task<PackageMetadata> GetPackageAsync(string packageType, string platform, string version, CancellationToken cancellationToken);
|
||||
Task<List<PackageMetadata>> GetPackagesAsync(string packageType, string platform, int top, bool includeToken, CancellationToken cancellationToken);
|
||||
Task<PackageMetadata> GetPackageAsync(string packageType, string platform, string version, bool includeToken, CancellationToken cancellationToken);
|
||||
|
||||
// agent update
|
||||
Task<TaskAgent> UpdateAgentUpdateStateAsync(int agentPoolId, int agentId, string currentState);
|
||||
|
||||
// runner authorization url
|
||||
Task<string> GetRunnerAuthUrlAsync(int runnerPoolId, int runnerId);
|
||||
Task ReportRunnerAuthUrlErrorAsync(int runnerPoolId, int runnerId, string error);
|
||||
Task<TaskAgent> UpdateAgentUpdateStateAsync(int agentPoolId, int agentId, string currentState, string trace);
|
||||
}
|
||||
|
||||
public sealed class RunnerServer : RunnerService, IRunnerServer
|
||||
@@ -256,6 +254,11 @@ namespace GitHub.Runner.Common
|
||||
return _genericTaskAgentClient.GetAgentsAsync(agentPoolId, agentName, false);
|
||||
}
|
||||
|
||||
public Task<List<TaskAgent>> GetAgentsAsync(string agentName)
|
||||
{
|
||||
return GetAgentsAsync(0, agentName); // search in all all agentPools
|
||||
}
|
||||
|
||||
public Task<TaskAgent> ReplaceAgentAsync(int agentPoolId, TaskAgent agent)
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.Generic);
|
||||
@@ -268,6 +271,11 @@ namespace GitHub.Runner.Common
|
||||
return _genericTaskAgentClient.DeleteAgentAsync(agentPoolId, agentId);
|
||||
}
|
||||
|
||||
public Task DeleteAgentAsync(int agentId)
|
||||
{
|
||||
return DeleteAgentAsync(0, agentId); // agentPool is ignored server side
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
// MessageQueue
|
||||
//-----------------------------------------------------------------
|
||||
@@ -300,10 +308,10 @@ namespace GitHub.Runner.Common
|
||||
// JobRequest
|
||||
//-----------------------------------------------------------------
|
||||
|
||||
public Task<TaskAgentJobRequest> RenewAgentRequestAsync(int poolId, long requestId, Guid lockToken, CancellationToken cancellationToken = default(CancellationToken))
|
||||
public Task<TaskAgentJobRequest> RenewAgentRequestAsync(int poolId, long requestId, Guid lockToken, string orchestrationId = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.JobRequest);
|
||||
return _requestTaskAgentClient.RenewAgentRequestAsync(poolId, requestId, lockToken, cancellationToken: cancellationToken);
|
||||
return _requestTaskAgentClient.RenewAgentRequestAsync(poolId, requestId, lockToken, orchestrationId: orchestrationId, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TaskAgentJobRequest> FinishAgentRequestAsync(int poolId, long requestId, Guid lockToken, DateTime finishTime, TaskResult result, CancellationToken cancellationToken = default(CancellationToken))
|
||||
@@ -321,37 +329,22 @@ namespace GitHub.Runner.Common
|
||||
//-----------------------------------------------------------------
|
||||
// Agent Package
|
||||
//-----------------------------------------------------------------
|
||||
public Task<List<PackageMetadata>> GetPackagesAsync(string packageType, string platform, int top, CancellationToken cancellationToken)
|
||||
public Task<List<PackageMetadata>> GetPackagesAsync(string packageType, string platform, int top, bool includeToken, CancellationToken cancellationToken)
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.Generic);
|
||||
return _genericTaskAgentClient.GetPackagesAsync(packageType, platform, top, cancellationToken: cancellationToken);
|
||||
return _genericTaskAgentClient.GetPackagesAsync(packageType, platform, top, includeToken, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public Task<PackageMetadata> GetPackageAsync(string packageType, string platform, string version, CancellationToken cancellationToken)
|
||||
public Task<PackageMetadata> GetPackageAsync(string packageType, string platform, string version, bool includeToken, CancellationToken cancellationToken)
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.Generic);
|
||||
return _genericTaskAgentClient.GetPackageAsync(packageType, platform, version, cancellationToken: cancellationToken);
|
||||
return _genericTaskAgentClient.GetPackageAsync(packageType, platform, version, includeToken, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TaskAgent> UpdateAgentUpdateStateAsync(int agentPoolId, int agentId, string currentState)
|
||||
public Task<TaskAgent> UpdateAgentUpdateStateAsync(int agentPoolId, int agentId, string currentState, string trace)
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.Generic);
|
||||
return _genericTaskAgentClient.UpdateAgentUpdateStateAsync(agentPoolId, agentId, currentState);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------
|
||||
// Runner Auth Url
|
||||
//-----------------------------------------------------------------
|
||||
public Task<string> GetRunnerAuthUrlAsync(int runnerPoolId, int runnerId)
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.MessageQueue);
|
||||
return _messageTaskAgentClient.GetAgentAuthUrlAsync(runnerPoolId, runnerId);
|
||||
}
|
||||
|
||||
public Task ReportRunnerAuthUrlErrorAsync(int runnerPoolId, int runnerId, string error)
|
||||
{
|
||||
CheckConnection(RunnerConnectionType.MessageQueue);
|
||||
return _messageTaskAgentClient.ReportAgentAuthUrlMigrationErrorAsync(runnerPoolId, runnerId, error);
|
||||
return _genericTaskAgentClient.UpdateAgentUpdateStateAsync(agentPoolId, agentId, currentState, trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,13 +96,14 @@ namespace GitHub.Runner.Common
|
||||
Trace.Info($"WRITE: {message}");
|
||||
if (!Silent)
|
||||
{
|
||||
if(colorCode != null)
|
||||
if (colorCode != null)
|
||||
{
|
||||
Console.ForegroundColor = colorCode.Value;
|
||||
Console.Write(message);
|
||||
Console.ResetColor();
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
Console.Write(message);
|
||||
}
|
||||
}
|
||||
@@ -120,13 +121,14 @@ namespace GitHub.Runner.Common
|
||||
Trace.Info($"WRITE LINE: {line}");
|
||||
if (!Silent)
|
||||
{
|
||||
if(colorCode != null)
|
||||
if (colorCode != null)
|
||||
{
|
||||
Console.ForegroundColor = colorCode.Value;
|
||||
Console.WriteLine(line);
|
||||
Console.ResetColor();
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
}
|
||||
}
|
||||
@@ -162,9 +164,8 @@ namespace GitHub.Runner.Common
|
||||
if (!Silent)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine($"# {message}");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine($"# {message}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
@@ -175,9 +176,8 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.Write("√ ");
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine(message);
|
||||
Console.ResetColor();
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
src/Runner.Common/Util/EncodingUtil.cs
Normal file
51
src/Runner.Common/Util/EncodingUtil.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Common.Util
|
||||
{
|
||||
public static class EncodingUtil
|
||||
{
|
||||
public static async Task SetEncoding(IHostContext hostContext, Tracing trace, CancellationToken cancellationToken)
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
try
|
||||
{
|
||||
if (Console.InputEncoding.CodePage != 65001)
|
||||
{
|
||||
using (var p = hostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
// Use UTF8 code page
|
||||
int exitCode = await p.ExecuteAsync(workingDirectory: hostContext.GetDirectory(WellKnownDirectory.Work),
|
||||
fileName: WhichUtil.Which("chcp", true, trace),
|
||||
arguments: "65001",
|
||||
environment: null,
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: null,
|
||||
killProcessOnCancel: false,
|
||||
redirectStandardIn: null,
|
||||
inheritConsoleHandler: true,
|
||||
cancellationToken: cancellationToken);
|
||||
if (exitCode == 0)
|
||||
{
|
||||
trace.Info("Successfully returned to code page 65001 (UTF8)");
|
||||
}
|
||||
else
|
||||
{
|
||||
trace.Warning($"'chcp 65001' failed with exit code {exitCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
trace.Warning($"'chcp 65001' failed with exception {ex.Message}");
|
||||
}
|
||||
#endif
|
||||
// Dummy variable to prevent compiler error CS1998: "This async method lacks 'await' operators and will run synchronously..."
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/Runner.Listener/Checks/ActionsCheck.cs
Normal file
93
src/Runner.Listener/Checks/ActionsCheck.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Listener.Check
|
||||
{
|
||||
public sealed class ActionsCheck : RunnerService, ICheckExtension
|
||||
{
|
||||
private string _logFile = null;
|
||||
|
||||
public int Order => 2;
|
||||
|
||||
public string CheckName => "GitHub Actions Connection";
|
||||
|
||||
public string CheckDescription => "Check if the Actions runner has access to the GitHub Actions service.";
|
||||
|
||||
public string CheckLog => _logFile;
|
||||
|
||||
public string HelpLink => "https://github.com/actions/runner/blob/main/docs/checks/actions.md";
|
||||
|
||||
public Type ExtensionType => typeof(ICheckExtension);
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
_logFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), StringUtil.Format("{0}_{1:yyyyMMdd-HHmmss}-utc.log", nameof(ActionsCheck), DateTime.UtcNow));
|
||||
}
|
||||
|
||||
// runner access to actions service
|
||||
public async Task<bool> RunCheck(string url, string pat)
|
||||
{
|
||||
await File.AppendAllLinesAsync(_logFile, HostContext.WarnLog());
|
||||
await File.AppendAllLinesAsync(_logFile, HostContext.CheckProxy());
|
||||
|
||||
var checkTasks = new List<Task<CheckResult>>();
|
||||
string githubApiUrl = null;
|
||||
string actionsTokenServiceUrl = null;
|
||||
string actionsPipelinesServiceUrl = null;
|
||||
var urlBuilder = new UriBuilder(url);
|
||||
if (UrlUtil.IsHostedServer(urlBuilder))
|
||||
{
|
||||
urlBuilder.Host = $"api.{urlBuilder.Host}";
|
||||
urlBuilder.Path = "";
|
||||
githubApiUrl = urlBuilder.Uri.AbsoluteUri;
|
||||
actionsTokenServiceUrl = "https://vstoken.actions.githubusercontent.com/_apis/health";
|
||||
actionsPipelinesServiceUrl = "https://pipelines.actions.githubusercontent.com/_apis/health";
|
||||
}
|
||||
else
|
||||
{
|
||||
urlBuilder.Path = "api/v3";
|
||||
githubApiUrl = urlBuilder.Uri.AbsoluteUri;
|
||||
urlBuilder.Path = "_services/vstoken/_apis/health";
|
||||
actionsTokenServiceUrl = urlBuilder.Uri.AbsoluteUri;
|
||||
urlBuilder.Path = "_services/pipelines/_apis/health";
|
||||
actionsPipelinesServiceUrl = urlBuilder.Uri.AbsoluteUri;
|
||||
}
|
||||
|
||||
// check github api
|
||||
checkTasks.Add(CheckUtil.CheckDns(githubApiUrl));
|
||||
checkTasks.Add(CheckUtil.CheckPing(githubApiUrl));
|
||||
checkTasks.Add(HostContext.CheckHttpsGetRequests(githubApiUrl, pat, expectedHeader: "X-GitHub-Request-Id"));
|
||||
|
||||
// check actions token service
|
||||
checkTasks.Add(CheckUtil.CheckDns(actionsTokenServiceUrl));
|
||||
checkTasks.Add(CheckUtil.CheckPing(actionsTokenServiceUrl));
|
||||
checkTasks.Add(HostContext.CheckHttpsGetRequests(actionsTokenServiceUrl, pat, expectedHeader: "x-vss-e2eid"));
|
||||
|
||||
// check actions pipelines service
|
||||
checkTasks.Add(CheckUtil.CheckDns(actionsPipelinesServiceUrl));
|
||||
checkTasks.Add(CheckUtil.CheckPing(actionsPipelinesServiceUrl));
|
||||
checkTasks.Add(HostContext.CheckHttpsGetRequests(actionsPipelinesServiceUrl, pat, expectedHeader: "x-vss-e2eid"));
|
||||
|
||||
// check HTTP POST to actions pipelines service
|
||||
checkTasks.Add(HostContext.CheckHttpsPostRequests(actionsPipelinesServiceUrl, pat, expectedHeader: "x-vss-e2eid"));
|
||||
|
||||
var result = true;
|
||||
while (checkTasks.Count > 0)
|
||||
{
|
||||
var finishedCheckTask = await Task.WhenAny<CheckResult>(checkTasks);
|
||||
var finishedCheck = await finishedCheckTask;
|
||||
result = result && finishedCheck.Pass;
|
||||
await File.AppendAllLinesAsync(_logFile, finishedCheck.Logs);
|
||||
checkTasks.Remove(finishedCheckTask);
|
||||
}
|
||||
|
||||
await Task.WhenAll(checkTasks);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
417
src/Runner.Listener/Checks/CheckUtil.cs
Normal file
417
src/Runner.Listener/Checks/CheckUtil.cs
Normal file
@@ -0,0 +1,417 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.Common;
|
||||
|
||||
namespace GitHub.Runner.Listener.Check
|
||||
{
|
||||
public static class CheckUtil
|
||||
{
|
||||
public static List<string> WarnLog(this IHostContext hostContext)
|
||||
{
|
||||
var logs = new List<string>();
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} **** !!! WARNING !!! ");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} **** DO NOT share the log in public place! The log may contains secrets in plain text. ");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} **** !!! WARNING !!! ");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
return logs;
|
||||
}
|
||||
|
||||
public static List<string> CheckProxy(this IHostContext hostContext)
|
||||
{
|
||||
var logs = new List<string>();
|
||||
if (!string.IsNullOrEmpty(hostContext.WebProxy.HttpProxyAddress) ||
|
||||
!string.IsNullOrEmpty(hostContext.WebProxy.HttpsProxyAddress))
|
||||
{
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} **** Runner is behind web proxy {hostContext.WebProxy.HttpsProxyAddress ?? hostContext.WebProxy.HttpProxyAddress} ");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
public static async Task<CheckResult> CheckDns(string targetUrl)
|
||||
{
|
||||
var result = new CheckResult();
|
||||
var url = new Uri(targetUrl);
|
||||
try
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Try DNS lookup for {url.Host} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
IPHostEntry host = await Dns.GetHostEntryAsync(url.Host);
|
||||
foreach (var address in host.AddressList)
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Resolved DNS for {url.Host} to '{address}'");
|
||||
}
|
||||
|
||||
result.Pass = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Resolved DNS for {url.Host} failed with error: {ex}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<CheckResult> CheckPing(string targetUrl)
|
||||
{
|
||||
var result = new CheckResult();
|
||||
var url = new Uri(targetUrl);
|
||||
try
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Try ping {url.Host} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
using (var ping = new Ping())
|
||||
{
|
||||
var reply = await ping.SendPingAsync(url.Host);
|
||||
if (reply.Status == IPStatus.Success)
|
||||
{
|
||||
result.Pass = true;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Ping {url.Host} ({reply.Address}) succeed within to '{reply.RoundtripTime} ms'");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Ping {url.Host} ({reply.Address}) failed with '{reply.Status}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Ping api.github.com failed with error: {ex}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<CheckResult> CheckHttpsGetRequests(this IHostContext hostContext, string url, string pat, string expectedHeader)
|
||||
{
|
||||
var result = new CheckResult();
|
||||
try
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Send HTTPS Request (GET) to {url} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
using (var _ = new HttpEventSourceListener(result.Logs))
|
||||
using (var httpClientHandler = hostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(hostContext.UserAgents);
|
||||
if (!string.IsNullOrEmpty(pat))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", pat);
|
||||
}
|
||||
|
||||
var response = await httpClient.GetAsync(url);
|
||||
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http status code: {response.StatusCode}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http response headers: {response.Headers}");
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http response body: {responseContent}");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
if (response.Headers.Contains(expectedHeader))
|
||||
{
|
||||
result.Pass = true;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http request 'GET' to {url} succeed");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http request 'GET' to {url} succeed but doesn't have expected HTTP response Header '{expectedHeader}'.");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http request 'GET' to {url} failed with {response.StatusCode}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Https request 'GET' to {url} failed with error: {ex}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<CheckResult> CheckHttpsPostRequests(this IHostContext hostContext, string url, string pat, string expectedHeader)
|
||||
{
|
||||
var result = new CheckResult();
|
||||
try
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Send HTTPS Request (POST) to {url} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
using (var _ = new HttpEventSourceListener(result.Logs))
|
||||
using (var httpClientHandler = hostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(hostContext.UserAgents);
|
||||
if (!string.IsNullOrEmpty(pat))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", pat);
|
||||
}
|
||||
|
||||
// Send empty JSON '{}' to service
|
||||
var response = await httpClient.PostAsJsonAsync<Dictionary<string, string>>(url, new Dictionary<string, string>());
|
||||
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http status code: {response.StatusCode}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http response headers: {response.Headers}");
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http response body: {responseContent}");
|
||||
if (response.Headers.Contains(expectedHeader))
|
||||
{
|
||||
result.Pass = true;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http request 'POST' to {url} has expected HTTP response header");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Http request 'POST' to {url} doesn't have expected HTTP response Header '{expectedHeader}'.");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Https request 'POST' to {url} failed with error: {ex}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<CheckResult> DownloadExtraCA(this IHostContext hostContext, string url, string pat)
|
||||
{
|
||||
var result = new CheckResult();
|
||||
try
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Download SSL Certificate from {url} ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
|
||||
var uri = new Uri(url);
|
||||
var env = new Dictionary<string, string>()
|
||||
{
|
||||
{ "HOSTNAME", uri.Host },
|
||||
{ "PORT", uri.IsDefaultPort ? (uri.Scheme.ToLowerInvariant() == "https" ? "443" : "80") : uri.Port.ToString() },
|
||||
{ "PATH", uri.AbsolutePath },
|
||||
{ "PAT", pat }
|
||||
};
|
||||
|
||||
var proxy = hostContext.WebProxy.GetProxy(uri);
|
||||
if (proxy != null)
|
||||
{
|
||||
env["PROXYHOST"] = proxy.Host;
|
||||
env["PROXYPORT"] = proxy.IsDefaultPort ? (proxy.Scheme.ToLowerInvariant() == "https" ? "443" : "80") : proxy.Port.ToString();
|
||||
if (hostContext.WebProxy.HttpProxyUsername != null ||
|
||||
hostContext.WebProxy.HttpsProxyUsername != null)
|
||||
{
|
||||
env["PROXYUSERNAME"] = hostContext.WebProxy.HttpProxyUsername ?? hostContext.WebProxy.HttpsProxyUsername;
|
||||
env["PROXYPASSWORD"] = hostContext.WebProxy.HttpProxyPassword ?? hostContext.WebProxy.HttpsProxyPassword;
|
||||
}
|
||||
else
|
||||
{
|
||||
env["PROXYUSERNAME"] = "";
|
||||
env["PROXYPASSWORD"] = "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
env["PROXYHOST"] = "";
|
||||
env["PROXYPORT"] = "";
|
||||
env["PROXYUSERNAME"] = "";
|
||||
env["PROXYPASSWORD"] = "";
|
||||
}
|
||||
|
||||
using (var processInvoker = hostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
processInvoker.OutputDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} [STDOUT] {args.Data}");
|
||||
}
|
||||
});
|
||||
|
||||
processInvoker.ErrorDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} [STDERR] {args.Data}");
|
||||
}
|
||||
});
|
||||
|
||||
var downloadCertScript = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Bin), "checkScripts", "downloadCert");
|
||||
var node12 = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), "node12", "bin", $"node{IOUtil.ExeExtension}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run '{node12} \"{downloadCertScript}\"' ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {StringUtil.ConvertToJson(env)}");
|
||||
await processInvoker.ExecuteAsync(
|
||||
hostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
node12,
|
||||
$"\"{downloadCertScript}\"",
|
||||
env,
|
||||
true,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
result.Pass = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Download SSL Certificate from '{url}' failed with error: {ex}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// EventSource listener for dotnet debug trace for HTTP and SSL
|
||||
public sealed class HttpEventSourceListener : EventListener
|
||||
{
|
||||
private readonly List<string> _logs;
|
||||
private readonly object _lock = new object();
|
||||
private readonly Dictionary<string, HashSet<string>> _ignoredEvent = new Dictionary<string, HashSet<string>>
|
||||
{
|
||||
{
|
||||
"Microsoft-System-Net-Http",
|
||||
new HashSet<string>
|
||||
{
|
||||
"Info",
|
||||
"Associate",
|
||||
"Enter",
|
||||
"Exit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Microsoft-System-Net-Security",
|
||||
new HashSet<string>
|
||||
{
|
||||
"Enter",
|
||||
"Exit",
|
||||
"Info",
|
||||
"DumpBuffer",
|
||||
"SslStreamCtor",
|
||||
"SecureChannelCtor",
|
||||
"NoDelegateNoClientCert",
|
||||
"CertsAfterFiltering",
|
||||
"UsingCachedCredential",
|
||||
"SspiSelectedCipherSuite"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public HttpEventSourceListener(List<string> logs)
|
||||
{
|
||||
_logs = logs;
|
||||
if (Environment.GetEnvironmentVariable("ACTIONS_RUNNER_TRACE_ALL_HTTP_EVENT") == "1")
|
||||
{
|
||||
_ignoredEvent.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnEventSourceCreated(EventSource eventSource)
|
||||
{
|
||||
base.OnEventSourceCreated(eventSource);
|
||||
|
||||
if (eventSource.Name == "Microsoft-System-Net-Http" ||
|
||||
eventSource.Name == "Microsoft-System-Net-Security")
|
||||
{
|
||||
EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnEventWritten(EventWrittenEventArgs eventData)
|
||||
{
|
||||
base.OnEventWritten(eventData);
|
||||
lock (_lock)
|
||||
{
|
||||
if (_ignoredEvent.TryGetValue(eventData.EventSource.Name, out var ignored) &&
|
||||
ignored.Contains(eventData.EventName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logs.Add($"{DateTime.UtcNow.ToString("O")} [START {eventData.EventSource.Name} - {eventData.EventName}]");
|
||||
_logs.AddRange(eventData.Payload.Select(x => string.Join(Environment.NewLine, x.ToString().Split(Environment.NewLine).Select(y => $"{DateTime.UtcNow.ToString("O")} {y}"))));
|
||||
_logs.Add($"{DateTime.UtcNow.ToString("O")} [END {eventData.EventSource.Name} - {eventData.EventName}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/Runner.Listener/Checks/GitCheck.cs
Normal file
171
src/Runner.Listener/Checks/GitCheck.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Listener.Check
|
||||
{
|
||||
public sealed class GitCheck : RunnerService, ICheckExtension
|
||||
{
|
||||
private string _logFile = null;
|
||||
private string _gitPath = null;
|
||||
|
||||
public int Order => 3;
|
||||
|
||||
public string CheckName => "Git Certificate/Proxy Validation";
|
||||
|
||||
public string CheckDescription => "Check if the Git CLI can access GitHub.com or GitHub Enterprise Server.";
|
||||
|
||||
public string CheckLog => _logFile;
|
||||
|
||||
public string HelpLink => "https://github.com/actions/runner/blob/main/docs/checks/git.md";
|
||||
|
||||
public Type ExtensionType => typeof(ICheckExtension);
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
_logFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), StringUtil.Format("{0}_{1:yyyyMMdd-HHmmss}-utc.log", nameof(GitCheck), DateTime.UtcNow));
|
||||
_gitPath = WhichUtil.Which("git");
|
||||
}
|
||||
|
||||
// git access to ghes/gh
|
||||
public async Task<bool> RunCheck(string url, string pat)
|
||||
{
|
||||
await File.AppendAllLinesAsync(_logFile, HostContext.WarnLog());
|
||||
await File.AppendAllLinesAsync(_logFile, HostContext.CheckProxy());
|
||||
|
||||
if (string.IsNullOrEmpty(_gitPath))
|
||||
{
|
||||
await File.AppendAllLinesAsync(_logFile, new[] { $"{DateTime.UtcNow.ToString("O")} Can't verify git with GitHub.com or GitHub Enterprise Server since git is not installed." });
|
||||
return false;
|
||||
}
|
||||
|
||||
var checkGit = await CheckGit(url, pat);
|
||||
var result = checkGit.Pass;
|
||||
await File.AppendAllLinesAsync(_logFile, checkGit.Logs);
|
||||
|
||||
// try fix SSL error by providing extra CA certificate.
|
||||
if (checkGit.SslError)
|
||||
{
|
||||
await File.AppendAllLinesAsync(_logFile, new[] { $"{DateTime.UtcNow.ToString("O")} Try fix SSL error by providing extra CA certificate." });
|
||||
var downloadCert = await HostContext.DownloadExtraCA(url, pat);
|
||||
await File.AppendAllLinesAsync(_logFile, downloadCert.Logs);
|
||||
|
||||
if (downloadCert.Pass)
|
||||
{
|
||||
var recheckGit = await CheckGit(url, pat, extraCA: true);
|
||||
await File.AppendAllLinesAsync(_logFile, recheckGit.Logs);
|
||||
if (recheckGit.Pass)
|
||||
{
|
||||
await File.AppendAllLinesAsync(_logFile, new[] { $"{DateTime.UtcNow.ToString("O")} Fixed SSL error by providing extra CA certs." });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<CheckResult> CheckGit(string url, string pat, bool extraCA = false)
|
||||
{
|
||||
var result = new CheckResult();
|
||||
try
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Validate server cert and proxy configuration with Git ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
var repoUrlBuilder = new UriBuilder(url);
|
||||
repoUrlBuilder.Path = "actions/checkout";
|
||||
repoUrlBuilder.UserName = "gh";
|
||||
repoUrlBuilder.Password = pat;
|
||||
|
||||
var gitProxy = "";
|
||||
var proxy = HostContext.WebProxy.GetProxy(repoUrlBuilder.Uri);
|
||||
if (proxy != null)
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Runner is behind http proxy '{proxy.AbsoluteUri}'");
|
||||
if (HostContext.WebProxy.HttpProxyUsername != null ||
|
||||
HostContext.WebProxy.HttpsProxyUsername != null)
|
||||
{
|
||||
var proxyUrlWithCred = UrlUtil.GetCredentialEmbeddedUrl(
|
||||
proxy,
|
||||
HostContext.WebProxy.HttpProxyUsername ?? HostContext.WebProxy.HttpsProxyUsername,
|
||||
HostContext.WebProxy.HttpProxyPassword ?? HostContext.WebProxy.HttpsProxyPassword);
|
||||
gitProxy = $"-c http.proxy={proxyUrlWithCred}";
|
||||
}
|
||||
else
|
||||
{
|
||||
gitProxy = $"-c http.proxy={proxy.AbsoluteUri}";
|
||||
}
|
||||
}
|
||||
|
||||
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
processInvoker.OutputDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {args.Data}");
|
||||
}
|
||||
});
|
||||
|
||||
processInvoker.ErrorDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {args.Data}");
|
||||
}
|
||||
});
|
||||
|
||||
var gitArgs = $"{gitProxy} ls-remote --exit-code {repoUrlBuilder.Uri.AbsoluteUri} HEAD";
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run 'git {gitArgs}' ");
|
||||
|
||||
var env = new Dictionary<string, string>
|
||||
{
|
||||
{ "GIT_TRACE", "1" },
|
||||
{ "GIT_CURL_VERBOSE", "1" }
|
||||
};
|
||||
|
||||
if (extraCA)
|
||||
{
|
||||
env["GIT_SSL_CAINFO"] = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), "download_ca_cert.pem");
|
||||
}
|
||||
|
||||
await processInvoker.ExecuteAsync(
|
||||
HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
_gitPath,
|
||||
gitArgs,
|
||||
env,
|
||||
true,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
result.Pass = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** git ls-remote failed with error: {ex}");
|
||||
if (result.Logs.Any(x => x.Contains("SSL Certificate problem", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** git ls-remote failed due to SSL cert issue.");
|
||||
result.SslError = true;
|
||||
}
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Runner.Listener/Checks/ICheckExtension.cs
Normal file
30
src/Runner.Listener/Checks/ICheckExtension.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Listener.Check
|
||||
{
|
||||
public interface ICheckExtension : IExtension
|
||||
{
|
||||
int Order { get; }
|
||||
string CheckName { get; }
|
||||
string CheckDescription { get; }
|
||||
string CheckLog { get; }
|
||||
string HelpLink { get; }
|
||||
Task<bool> RunCheck(string url, string pat);
|
||||
}
|
||||
|
||||
public class CheckResult
|
||||
{
|
||||
public CheckResult()
|
||||
{
|
||||
Logs = new List<string>();
|
||||
}
|
||||
|
||||
public bool Pass { get; set; }
|
||||
|
||||
public bool SslError { get; set; }
|
||||
|
||||
public List<string> Logs { get; set; }
|
||||
}
|
||||
}
|
||||
59
src/Runner.Listener/Checks/InternetCheck.cs
Normal file
59
src/Runner.Listener/Checks/InternetCheck.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Listener.Check
|
||||
{
|
||||
public sealed class InternetCheck : RunnerService, ICheckExtension
|
||||
{
|
||||
private string _logFile = null;
|
||||
|
||||
public int Order => 1;
|
||||
|
||||
public string CheckName => "Internet Connection";
|
||||
|
||||
public string CheckDescription => "Check if the Actions runner has internet access.";
|
||||
|
||||
public string CheckLog => _logFile;
|
||||
|
||||
public string HelpLink => "https://github.com/actions/runner/blob/main/docs/checks/internet.md";
|
||||
|
||||
public Type ExtensionType => typeof(ICheckExtension);
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
_logFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), StringUtil.Format("{0}_{1:yyyyMMdd-HHmmss}-utc.log", nameof(InternetCheck), DateTime.UtcNow));
|
||||
}
|
||||
|
||||
// check runner access to api.github.com
|
||||
public async Task<bool> RunCheck(string url, string pat)
|
||||
{
|
||||
await File.AppendAllLinesAsync(_logFile, HostContext.WarnLog());
|
||||
await File.AppendAllLinesAsync(_logFile, HostContext.CheckProxy());
|
||||
|
||||
var checkTasks = new List<Task<CheckResult>>();
|
||||
checkTasks.Add(CheckUtil.CheckDns("https://api.github.com"));
|
||||
checkTasks.Add(CheckUtil.CheckPing("https://api.github.com"));
|
||||
|
||||
// We don't need to pass a PAT since it might be a token for GHES.
|
||||
checkTasks.Add(HostContext.CheckHttpsGetRequests("https://api.github.com", pat: null, expectedHeader: "X-GitHub-Request-Id"));
|
||||
|
||||
var result = true;
|
||||
while (checkTasks.Count > 0)
|
||||
{
|
||||
var finishedCheckTask = await Task.WhenAny<CheckResult>(checkTasks);
|
||||
var finishedCheck = await finishedCheckTask;
|
||||
result = result && finishedCheck.Pass;
|
||||
await File.AppendAllLinesAsync(_logFile, finishedCheck.Logs);
|
||||
checkTasks.Remove(finishedCheckTask);
|
||||
}
|
||||
|
||||
await Task.WhenAll(checkTasks);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
181
src/Runner.Listener/Checks/NodeJsCheck.cs
Normal file
181
src/Runner.Listener/Checks/NodeJsCheck.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Listener.Check
|
||||
{
|
||||
public sealed class NodeJsCheck : RunnerService, ICheckExtension
|
||||
{
|
||||
private string _logFile = null;
|
||||
|
||||
public int Order => 4;
|
||||
|
||||
public string CheckName => "Node.js Certificate/Proxy Validation";
|
||||
|
||||
public string CheckDescription => "Check if Node.js has access to GitHub.com or GitHub Enterprise Server.";
|
||||
|
||||
public string CheckLog => _logFile;
|
||||
|
||||
public string HelpLink => "https://github.com/actions/runner/blob/main/docs/checks/nodejs.md";
|
||||
|
||||
public Type ExtensionType => typeof(ICheckExtension);
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
_logFile = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Diag), StringUtil.Format("{0}_{1:yyyyMMdd-HHmmss}-utc.log", nameof(NodeJsCheck), DateTime.UtcNow));
|
||||
}
|
||||
|
||||
// node access to ghes/gh
|
||||
public async Task<bool> RunCheck(string url, string pat)
|
||||
{
|
||||
await File.AppendAllLinesAsync(_logFile, HostContext.WarnLog());
|
||||
await File.AppendAllLinesAsync(_logFile, HostContext.CheckProxy());
|
||||
|
||||
// Request to github.com or ghes server
|
||||
var urlBuilder = new UriBuilder(url);
|
||||
if (UrlUtil.IsHostedServer(urlBuilder))
|
||||
{
|
||||
urlBuilder.Host = $"api.{urlBuilder.Host}";
|
||||
urlBuilder.Path = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
urlBuilder.Path = "api/v3";
|
||||
}
|
||||
|
||||
var checkNode = await CheckNodeJs(urlBuilder.Uri.AbsoluteUri, pat);
|
||||
var result = checkNode.Pass;
|
||||
await File.AppendAllLinesAsync(_logFile, checkNode.Logs);
|
||||
|
||||
// try fix SSL error by providing extra CA certificate.
|
||||
if (checkNode.SslError)
|
||||
{
|
||||
var downloadCert = await HostContext.DownloadExtraCA(urlBuilder.Uri.AbsoluteUri, pat);
|
||||
await File.AppendAllLinesAsync(_logFile, downloadCert.Logs);
|
||||
|
||||
if (downloadCert.Pass)
|
||||
{
|
||||
var recheckNode = await CheckNodeJs(urlBuilder.Uri.AbsoluteUri, pat, extraCA: true);
|
||||
await File.AppendAllLinesAsync(_logFile, recheckNode.Logs);
|
||||
if (recheckNode.Pass)
|
||||
{
|
||||
await File.AppendAllLinesAsync(_logFile, new[] { $"{DateTime.UtcNow.ToString("O")} Fixed SSL error by providing extra CA certs." });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<CheckResult> CheckNodeJs(string url, string pat, bool extraCA = false)
|
||||
{
|
||||
var result = new CheckResult();
|
||||
try
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Make Http request to {url} using node.js ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
|
||||
// Request to github.com or ghes server
|
||||
Uri requestUrl = new Uri(url);
|
||||
var env = new Dictionary<string, string>()
|
||||
{
|
||||
{ "HOSTNAME", requestUrl.Host },
|
||||
{ "PORT", requestUrl.IsDefaultPort ? (requestUrl.Scheme.ToLowerInvariant() == "https" ? "443" : "80") : requestUrl.Port.ToString() },
|
||||
{ "PATH", requestUrl.AbsolutePath },
|
||||
{ "PAT", pat }
|
||||
};
|
||||
|
||||
var proxy = HostContext.WebProxy.GetProxy(requestUrl);
|
||||
if (proxy != null)
|
||||
{
|
||||
env["PROXYHOST"] = proxy.Host;
|
||||
env["PROXYPORT"] = proxy.IsDefaultPort ? (proxy.Scheme.ToLowerInvariant() == "https" ? "443" : "80") : proxy.Port.ToString();
|
||||
if (HostContext.WebProxy.HttpProxyUsername != null ||
|
||||
HostContext.WebProxy.HttpsProxyUsername != null)
|
||||
{
|
||||
env["PROXYUSERNAME"] = HostContext.WebProxy.HttpProxyUsername ?? HostContext.WebProxy.HttpsProxyUsername;
|
||||
env["PROXYPASSWORD"] = HostContext.WebProxy.HttpProxyPassword ?? HostContext.WebProxy.HttpsProxyPassword;
|
||||
}
|
||||
else
|
||||
{
|
||||
env["PROXYUSERNAME"] = "";
|
||||
env["PROXYPASSWORD"] = "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
env["PROXYHOST"] = "";
|
||||
env["PROXYPORT"] = "";
|
||||
env["PROXYUSERNAME"] = "";
|
||||
env["PROXYPASSWORD"] = "";
|
||||
}
|
||||
|
||||
if (extraCA)
|
||||
{
|
||||
env["NODE_EXTRA_CA_CERTS"] = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), "download_ca_cert.pem");
|
||||
}
|
||||
|
||||
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
processInvoker.OutputDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} [STDOUT] {args.Data}");
|
||||
}
|
||||
});
|
||||
|
||||
processInvoker.ErrorDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} [STDERR] {args.Data}");
|
||||
}
|
||||
});
|
||||
|
||||
var makeWebRequestScript = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "checkScripts", "makeWebRequest.js");
|
||||
var node12 = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "node12", "bin", $"node{IOUtil.ExeExtension}");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} Run '{node12} \"{makeWebRequestScript}\"' ");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} {StringUtil.ConvertToJson(env)}");
|
||||
await processInvoker.ExecuteAsync(
|
||||
HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||
node12,
|
||||
$"\"{makeWebRequestScript}\"",
|
||||
env,
|
||||
true,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
result.Pass = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Pass = false;
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Make https request to {url} using node.js failed with error: {ex}");
|
||||
if (result.Logs.Any(x => x.Contains("UNABLE_TO_VERIFY_LEAF_SIGNATURE") ||
|
||||
x.Contains("UNABLE_TO_GET_ISSUER_CERT_LOCALLY") ||
|
||||
x.Contains("SELF_SIGNED_CERT_IN_CHAIN")))
|
||||
{
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** Https request failed due to SSL cert issue.");
|
||||
result.SslError = true;
|
||||
}
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} **** ****");
|
||||
result.Logs.Add($"{DateTime.UtcNow.ToString("O")} ***************************************************************************************************************");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,13 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
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.Once,
|
||||
Constants.Runner.CommandLine.Flags.Replace,
|
||||
Constants.Runner.CommandLine.Flags.RunAsService,
|
||||
Constants.Runner.CommandLine.Flags.Once,
|
||||
Constants.Runner.CommandLine.Flags.Unattended,
|
||||
Constants.Runner.CommandLine.Flags.Version
|
||||
};
|
||||
@@ -39,9 +41,11 @@ namespace GitHub.Runner.Listener
|
||||
private readonly string[] validArgs =
|
||||
{
|
||||
Constants.Runner.CommandLine.Args.Auth,
|
||||
Constants.Runner.CommandLine.Args.Labels,
|
||||
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
|
||||
Constants.Runner.CommandLine.Args.Name,
|
||||
Constants.Runner.CommandLine.Args.Pool,
|
||||
Constants.Runner.CommandLine.Args.PAT,
|
||||
Constants.Runner.CommandLine.Args.RunnerGroup,
|
||||
Constants.Runner.CommandLine.Args.StartupType,
|
||||
Constants.Runner.CommandLine.Args.Token,
|
||||
Constants.Runner.CommandLine.Args.Url,
|
||||
@@ -58,11 +62,14 @@ namespace GitHub.Runner.Listener
|
||||
public bool Warmup => TestCommand(Constants.Runner.CommandLine.Commands.Warmup);
|
||||
|
||||
// Flags.
|
||||
public bool Check => TestFlag(Constants.Runner.CommandLine.Flags.Check);
|
||||
public bool Commit => TestFlag(Constants.Runner.CommandLine.Flags.Commit);
|
||||
public bool Help => TestFlag(Constants.Runner.CommandLine.Flags.Help);
|
||||
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
|
||||
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
|
||||
public bool Ephemeral => TestFlag(Constants.Runner.CommandLine.Flags.Ephemeral);
|
||||
|
||||
// Keep this around since customers still relies on it
|
||||
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
|
||||
|
||||
// Constructor.
|
||||
@@ -168,6 +175,15 @@ namespace GitHub.Runner.Listener
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
|
||||
public string GetRunnerGroupName(string defaultPoolName = null)
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.RunnerGroup,
|
||||
description: "Enter the name of the runner group to add this runner to:",
|
||||
defaultValue: defaultPoolName ?? "default",
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
|
||||
public string GetToken()
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
@@ -177,6 +193,22 @@ namespace GitHub.Runner.Listener
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
|
||||
public string GetGitHubPersonalAccessToken(bool required = false)
|
||||
{
|
||||
if (required)
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.PAT,
|
||||
description: "What is your GitHub personal access token?",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.NonEmptyValidator);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetArg(name: Constants.Runner.CommandLine.Args.PAT);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetRunnerRegisterToken()
|
||||
{
|
||||
return GetArgOrPrompt(
|
||||
@@ -249,6 +281,24 @@ namespace GitHub.Runner.Listener
|
||||
return GetArg(Constants.Runner.CommandLine.Args.StartupType);
|
||||
}
|
||||
|
||||
public ISet<string> GetLabels()
|
||||
{
|
||||
var labelSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
string labels = GetArgOrPrompt(
|
||||
name: Constants.Runner.CommandLine.Args.Labels,
|
||||
description: $"This runner will have the following labels: 'self-hosted', '{VarUtil.OS}', '{VarUtil.OSArchitecture}' \nEnter any additional labels (ex. label-1,label-2):",
|
||||
defaultValue: string.Empty,
|
||||
validator: Validators.LabelsValidator,
|
||||
isOptional: true);
|
||||
|
||||
if (!string.IsNullOrEmpty(labels))
|
||||
{
|
||||
labelSet = labels.Split(',').Where(x => !string.IsNullOrEmpty(x)).ToHashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return labelSet;
|
||||
}
|
||||
|
||||
//
|
||||
// Private helpers.
|
||||
//
|
||||
@@ -280,7 +330,8 @@ namespace GitHub.Runner.Listener
|
||||
string name,
|
||||
string description,
|
||||
string defaultValue,
|
||||
Func<string, bool> validator)
|
||||
Func<string, bool> validator,
|
||||
bool isOptional = false)
|
||||
{
|
||||
// Check for the arg in the command line parser.
|
||||
ArgUtil.NotNull(validator, nameof(validator));
|
||||
@@ -311,7 +362,8 @@ namespace GitHub.Runner.Listener
|
||||
secret: Constants.Runner.CommandLine.Args.Secrets.Any(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase)),
|
||||
defaultValue: defaultValue,
|
||||
validator: validator,
|
||||
unattended: Unattended);
|
||||
unattended: Unattended,
|
||||
isOptional: isOptional);
|
||||
}
|
||||
|
||||
private string GetEnvArg(string name)
|
||||
|
||||
@@ -4,7 +4,6 @@ using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.OAuth;
|
||||
using GitHub.Services.WebApi;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -12,6 +11,7 @@ using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GitHub.Runner.Listener.Configuration
|
||||
@@ -22,6 +22,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
bool IsConfigured();
|
||||
Task ConfigureAsync(CommandSettings command);
|
||||
Task UnconfigureAsync(CommandSettings command);
|
||||
void DeleteLocalRunnerConfig();
|
||||
RunnerSettings LoadSettings();
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
Trace.Info(nameof(LoadSettings));
|
||||
if (!IsConfigured())
|
||||
{
|
||||
throw new InvalidOperationException("Not configured");
|
||||
throw new InvalidOperationException("Not configured. Run config.(sh/cmd) to configure the runner.");
|
||||
}
|
||||
|
||||
RunnerSettings settings = _store.GetSettings();
|
||||
@@ -65,18 +66,18 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
public async Task ConfigureAsync(CommandSettings command)
|
||||
{
|
||||
_term.WriteLine();
|
||||
_term.WriteLine("--------------------------------------------------------------------------------", ConsoleColor.White);
|
||||
_term.WriteLine("| ____ _ _ _ _ _ _ _ _ |", ConsoleColor.White);
|
||||
_term.WriteLine("| / ___(_) |_| | | |_ _| |__ / \\ ___| |_(_) ___ _ __ ___ |", ConsoleColor.White);
|
||||
_term.WriteLine("| | | _| | __| |_| | | | | '_ \\ / _ \\ / __| __| |/ _ \\| '_ \\/ __| |", ConsoleColor.White);
|
||||
_term.WriteLine("| | |_| | | |_| _ | |_| | |_) | / ___ \\ (__| |_| | (_) | | | \\__ \\ |", ConsoleColor.White);
|
||||
_term.WriteLine("| \\____|_|\\__|_| |_|\\__,_|_.__/ /_/ \\_\\___|\\__|_|\\___/|_| |_|___/ |", ConsoleColor.White);
|
||||
_term.WriteLine("| |", ConsoleColor.White);
|
||||
_term.Write("| ", ConsoleColor.White);
|
||||
_term.WriteLine("--------------------------------------------------------------------------------");
|
||||
_term.WriteLine("| ____ _ _ _ _ _ _ _ _ |");
|
||||
_term.WriteLine("| / ___(_) |_| | | |_ _| |__ / \\ ___| |_(_) ___ _ __ ___ |");
|
||||
_term.WriteLine("| | | _| | __| |_| | | | | '_ \\ / _ \\ / __| __| |/ _ \\| '_ \\/ __| |");
|
||||
_term.WriteLine("| | |_| | | |_| _ | |_| | |_) | / ___ \\ (__| |_| | (_) | | | \\__ \\ |");
|
||||
_term.WriteLine("| \\____|_|\\__|_| |_|\\__,_|_.__/ /_/ \\_\\___|\\__|_|\\___/|_| |_|___/ |");
|
||||
_term.WriteLine("| |");
|
||||
_term.Write("| ");
|
||||
_term.Write("Self-hosted runner registration", ConsoleColor.Cyan);
|
||||
_term.WriteLine(" |", ConsoleColor.White);
|
||||
_term.WriteLine("| |", ConsoleColor.White);
|
||||
_term.WriteLine("--------------------------------------------------------------------------------", ConsoleColor.White);
|
||||
_term.WriteLine(" |");
|
||||
_term.WriteLine("| |");
|
||||
_term.WriteLine("--------------------------------------------------------------------------------");
|
||||
|
||||
Trace.Info(nameof(ConfigureAsync));
|
||||
if (IsConfigured())
|
||||
@@ -86,17 +87,17 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
RunnerSettings runnerSettings = new RunnerSettings();
|
||||
|
||||
bool isHostedServer = false;
|
||||
// Loop getting url and creds until you can connect
|
||||
ICredentialProvider credProvider = null;
|
||||
VssCredentials creds = null;
|
||||
_term.WriteSection("Authentication");
|
||||
while (true)
|
||||
{
|
||||
// Get the URL
|
||||
// When testing against a dev deployment of Actions Service, set this environment variable
|
||||
var useDevActionsServiceUrl = Environment.GetEnvironmentVariable("USE_DEV_ACTIONS_SERVICE_URL");
|
||||
var inputUrl = command.GetUrl();
|
||||
if (!inputUrl.Contains("github.com", StringComparison.OrdinalIgnoreCase) &&
|
||||
!inputUrl.Contains("github.localhost", StringComparison.OrdinalIgnoreCase))
|
||||
if (inputUrl.Contains("codedev.ms", StringComparison.OrdinalIgnoreCase)
|
||||
|| useDevActionsServiceUrl != null)
|
||||
{
|
||||
runnerSettings.ServerUrl = inputUrl;
|
||||
// Get the credentials
|
||||
@@ -107,8 +108,8 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
else
|
||||
{
|
||||
runnerSettings.GitHubUrl = inputUrl;
|
||||
var githubToken = command.GetRunnerRegisterToken();
|
||||
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, githubToken, Constants.RunnerEvent.Register);
|
||||
var registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration");
|
||||
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register);
|
||||
runnerSettings.ServerUrl = authResult.TenantUrl;
|
||||
creds = authResult.ToVssCredentials();
|
||||
Trace.Info("cred retrieved via GitHub auth");
|
||||
@@ -117,7 +118,21 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
try
|
||||
{
|
||||
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
|
||||
isHostedServer = await IsHostedServer(runnerSettings.ServerUrl, creds);
|
||||
// Hosted usually means github.com or localhost, while OnPremises means GHES or GHAE
|
||||
runnerSettings.IsHostedServer = runnerSettings.GitHubUrl == null || UrlUtil.IsHostedServer(new UriBuilder(runnerSettings.GitHubUrl));
|
||||
|
||||
// Warn if the Actions server url and GHES server url has different Host
|
||||
if (!runnerSettings.IsHostedServer)
|
||||
{
|
||||
// Example actionsServerUrl is https://my-ghes/_services/pipelines/[...]
|
||||
// Example githubServerUrl is https://my-ghes
|
||||
var actionsServerUrl = new Uri(runnerSettings.ServerUrl);
|
||||
var githubServerUrl = new Uri(runnerSettings.GitHubUrl);
|
||||
if (!string.Equals(actionsServerUrl.Authority, githubServerUrl.Authority, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"GitHub Actions is not properly configured in GHES. GHES url: {runnerSettings.GitHubUrl}, Actions url: {runnerSettings.ServerUrl}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate can connect.
|
||||
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), creds);
|
||||
@@ -146,17 +161,34 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
_term.WriteSection("Runner Registration");
|
||||
|
||||
//Get all the agent pools, and select the first private pool
|
||||
// If we have more than one runner group available, allow the user to specify which one to be added into
|
||||
string poolName = null;
|
||||
TaskAgentPool agentPool = null;
|
||||
List<TaskAgentPool> agentPools = await _runnerServer.GetAgentPoolsAsync();
|
||||
TaskAgentPool agentPool = agentPools?.Where(x => x.IsHosted == false).FirstOrDefault();
|
||||
TaskAgentPool defaultPool = agentPools?.Where(x => x.IsInternal).FirstOrDefault();
|
||||
|
||||
if (agentPool == null)
|
||||
if (agentPools?.Where(x => !x.IsHosted).Count() > 0)
|
||||
{
|
||||
throw new TaskAgentPoolNotFoundException($"Could not find any private pool. Contact support.");
|
||||
poolName = command.GetRunnerGroupName(defaultPool?.Name);
|
||||
_term.WriteLine();
|
||||
agentPool = agentPools.Where(x => string.Equals(poolName, x.Name, StringComparison.OrdinalIgnoreCase) && !x.IsHosted).FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info("Found a private pool with id {1} and name {2}", agentPool.Id, agentPool.Name);
|
||||
agentPool = defaultPool;
|
||||
}
|
||||
|
||||
if (agentPool == null && poolName == null)
|
||||
{
|
||||
throw new TaskAgentPoolNotFoundException($"Could not find any self-hosted runner groups. Contact support.");
|
||||
}
|
||||
else if (agentPool == null && poolName != null)
|
||||
{
|
||||
throw new TaskAgentPoolNotFoundException($"Could not find any self-hosted runner group named \"{poolName}\".");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info($"Found a self-hosted runner group with id {agentPool.Id} and name {agentPool.Name}");
|
||||
runnerSettings.PoolId = agentPool.Id;
|
||||
runnerSettings.PoolName = agentPool.Name;
|
||||
}
|
||||
@@ -164,10 +196,14 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
TaskAgent agent;
|
||||
while (true)
|
||||
{
|
||||
runnerSettings.Ephemeral = command.Ephemeral;
|
||||
runnerSettings.AgentName = command.GetRunnerName();
|
||||
|
||||
_term.WriteLine();
|
||||
|
||||
var userLabels = command.GetLabels();
|
||||
_term.WriteLine();
|
||||
|
||||
var agents = await _runnerServer.GetAgentsAsync(runnerSettings.PoolId, runnerSettings.AgentName);
|
||||
Trace.Verbose("Returns {0} agents", agents.Count);
|
||||
agent = agents.FirstOrDefault();
|
||||
@@ -177,7 +213,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
if (command.GetReplace())
|
||||
{
|
||||
// Update existing agent with new PublicKey, agent version.
|
||||
agent = UpdateExistingAgent(agent, publicKey);
|
||||
agent = UpdateExistingAgent(agent, publicKey, userLabels, runnerSettings.Ephemeral);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -194,13 +230,13 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
else if (command.Unattended)
|
||||
{
|
||||
// if not replace and it is unattended config.
|
||||
throw new TaskAgentExistsException($"Pool {runnerSettings.PoolId} already contains a runner with name {runnerSettings.AgentName}.");
|
||||
throw new TaskAgentExistsException($"A runner exists with the same name {runnerSettings.AgentName}.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a new agent.
|
||||
agent = CreateNewAgent(runnerSettings.AgentName, publicKey);
|
||||
// Create a new agent.
|
||||
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels, runnerSettings.Ephemeral);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -218,44 +254,11 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
// Add Agent Id to settings
|
||||
runnerSettings.AgentId = agent.Id;
|
||||
|
||||
// respect the serverUrl resolve by server.
|
||||
// in case of agent configured using collection url instead of account url.
|
||||
string agentServerUrl;
|
||||
if (agent.Properties.TryGetValidatedValue<string>("ServerUrl", out agentServerUrl) &&
|
||||
!string.IsNullOrEmpty(agentServerUrl))
|
||||
{
|
||||
Trace.Info($"Agent server url resolve by server: '{agentServerUrl}'.");
|
||||
|
||||
// we need make sure the Schema/Host/Port component of the url remain the same.
|
||||
UriBuilder inputServerUrl = new UriBuilder(runnerSettings.ServerUrl);
|
||||
UriBuilder serverReturnedServerUrl = new UriBuilder(agentServerUrl);
|
||||
if (Uri.Compare(inputServerUrl.Uri, serverReturnedServerUrl.Uri, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
inputServerUrl.Path = serverReturnedServerUrl.Path;
|
||||
Trace.Info($"Replace server returned url's scheme://host:port component with user input server url's scheme://host:port: '{inputServerUrl.Uri.AbsoluteUri}'.");
|
||||
runnerSettings.ServerUrl = inputServerUrl.Uri.AbsoluteUri;
|
||||
}
|
||||
else
|
||||
{
|
||||
runnerSettings.ServerUrl = agentServerUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// See if the server supports our OAuth key exchange for credentials
|
||||
if (agent.Authorization != null &&
|
||||
agent.Authorization.ClientId != Guid.Empty &&
|
||||
agent.Authorization.AuthorizationUrl != null)
|
||||
{
|
||||
UriBuilder configServerUrl = new UriBuilder(runnerSettings.ServerUrl);
|
||||
UriBuilder oauthEndpointUrlBuilder = new UriBuilder(agent.Authorization.AuthorizationUrl);
|
||||
if (!isHostedServer && Uri.Compare(configServerUrl.Uri, oauthEndpointUrlBuilder.Uri, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
oauthEndpointUrlBuilder.Scheme = configServerUrl.Scheme;
|
||||
oauthEndpointUrlBuilder.Host = configServerUrl.Host;
|
||||
oauthEndpointUrlBuilder.Port = configServerUrl.Port;
|
||||
Trace.Info($"Set oauth endpoint url's scheme://host:port component to match runner configure url's scheme://host:port: '{oauthEndpointUrlBuilder.Uri.AbsoluteUri}'.");
|
||||
}
|
||||
|
||||
var credentialData = new CredentialData
|
||||
{
|
||||
Scheme = Constants.Configuration.OAuth,
|
||||
@@ -263,7 +266,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
{ "clientId", agent.Authorization.ClientId.ToString("D") },
|
||||
{ "authorizationUrl", agent.Authorization.AuthorizationUrl.AbsoluteUri },
|
||||
{ "oauthEndpointUrl", oauthEndpointUrlBuilder.Uri.AbsoluteUri },
|
||||
{ "requireFipsCryptography", agent.Properties.GetValue("RequireFipsCryptography", false).ToString() }
|
||||
},
|
||||
};
|
||||
|
||||
@@ -291,7 +294,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
// there are two exception messages server send that indicate clock skew.
|
||||
// 1. The bearer token expired on {jwt.ValidTo}. Current server time is {DateTime.UtcNow}.
|
||||
// 2. The bearer token is not valid until {jwt.ValidFrom}. Current server time is {DateTime.UtcNow}.
|
||||
// 2. The bearer token is not valid until {jwt.ValidFrom}. Current server time is {DateTime.UtcNow}.
|
||||
Trace.Error("Catch exception during test agent connection.");
|
||||
Trace.Error(ex);
|
||||
throw new Exception("The local machine's clock may be out of sync with the server time by more than five minutes. Please sync your clock with your domain or internet time and try again.");
|
||||
@@ -327,6 +330,38 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
#endif
|
||||
}
|
||||
|
||||
// Delete .runner and .credentials files
|
||||
public void DeleteLocalRunnerConfig()
|
||||
{
|
||||
bool isConfigured = _store.IsConfigured();
|
||||
bool hasCredentials = _store.HasCredentials();
|
||||
//delete credential config files
|
||||
var currentAction = "Removing .credentials";
|
||||
if (hasCredentials)
|
||||
{
|
||||
_store.DeleteCredential();
|
||||
var keyManager = HostContext.GetService<IRSAKeyManager>();
|
||||
keyManager.DeleteKey();
|
||||
_term.WriteSuccessMessage("Removed .credentials");
|
||||
}
|
||||
else
|
||||
{
|
||||
_term.WriteLine("Does not exist. Skipping " + currentAction);
|
||||
}
|
||||
|
||||
//delete settings config file
|
||||
currentAction = "Removing .runner";
|
||||
if (isConfigured)
|
||||
{
|
||||
_store.DeleteSettings();
|
||||
_term.WriteSuccessMessage("Removed .runner");
|
||||
}
|
||||
else
|
||||
{
|
||||
_term.WriteLine("Does not exist. Skipping " + currentAction);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UnconfigureAsync(CommandSettings command)
|
||||
{
|
||||
string currentAction = string.Empty;
|
||||
@@ -346,12 +381,9 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
_term.WriteLine();
|
||||
_term.WriteSuccessMessage("Runner service removed");
|
||||
#elif OS_LINUX
|
||||
// unconfig system D service first
|
||||
throw new Exception("Unconfigure service first");
|
||||
#elif OS_OSX
|
||||
// unconfig osx service first
|
||||
throw new Exception("Unconfigure service first");
|
||||
#else
|
||||
// unconfig systemd or osx service first
|
||||
throw new Exception("Uninstall service first");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -374,17 +406,16 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
else
|
||||
{
|
||||
var githubToken = command.GetRunnerDeletionToken();
|
||||
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, githubToken, Constants.RunnerEvent.Remove);
|
||||
var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove");
|
||||
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, deletionToken, Constants.RunnerEvent.Remove);
|
||||
creds = authResult.ToVssCredentials();
|
||||
Trace.Info("cred retrieved via GitHub auth");
|
||||
}
|
||||
|
||||
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
|
||||
bool isHostedServer = await IsHostedServer(settings.ServerUrl, creds);
|
||||
await _runnerServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
|
||||
|
||||
var agents = await _runnerServer.GetAgentsAsync(settings.PoolId, settings.AgentName);
|
||||
var agents = await _runnerServer.GetAgentsAsync(settings.AgentName);
|
||||
Trace.Verbose("Returns {0} agents", agents.Count);
|
||||
TaskAgent agent = agents.FirstOrDefault();
|
||||
if (agent == null)
|
||||
@@ -393,7 +424,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
else
|
||||
{
|
||||
await _runnerServer.DeleteAgentAsync(settings.PoolId, settings.AgentId);
|
||||
await _runnerServer.DeleteAgentAsync(settings.AgentId);
|
||||
|
||||
_term.WriteLine();
|
||||
_term.WriteSuccessMessage("Runner removed successfully");
|
||||
@@ -404,31 +435,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
_term.WriteLine("Cannot connect to server, because config files are missing. Skipping removing runner from the server.");
|
||||
}
|
||||
|
||||
//delete credential config files
|
||||
currentAction = "Removing .credentials";
|
||||
if (hasCredentials)
|
||||
{
|
||||
_store.DeleteCredential();
|
||||
var keyManager = HostContext.GetService<IRSAKeyManager>();
|
||||
keyManager.DeleteKey();
|
||||
_term.WriteSuccessMessage("Removed .credentials");
|
||||
}
|
||||
else
|
||||
{
|
||||
_term.WriteLine("Does not exist. Skipping " + currentAction);
|
||||
}
|
||||
|
||||
//delete settings config file
|
||||
currentAction = "Removing .runner";
|
||||
if (isConfigured)
|
||||
{
|
||||
_store.DeleteSettings();
|
||||
_term.WriteSuccessMessage("Removed .runner");
|
||||
}
|
||||
else
|
||||
{
|
||||
_term.WriteLine("Does not exist. Skipping " + currentAction);
|
||||
}
|
||||
DeleteLocalRunnerConfig();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -459,7 +466,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
|
||||
|
||||
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey)
|
||||
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral)
|
||||
{
|
||||
ArgUtil.NotNull(agent, nameof(agent));
|
||||
agent.Authorization = new TaskAgentAuthorization
|
||||
@@ -467,18 +474,27 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
PublicKey = new TaskAgentPublicKey(publicKey.Exponent, publicKey.Modulus),
|
||||
};
|
||||
|
||||
// update - update instead of delete so we don't lose labels etc...
|
||||
// update should replace the existing labels
|
||||
agent.Version = BuildConstants.RunnerPackage.Version;
|
||||
agent.OSDescription = RuntimeInformation.OSDescription;
|
||||
agent.Ephemeral = ephemeral;
|
||||
agent.MaxParallelism = 1;
|
||||
|
||||
agent.Labels.Add("self-hosted");
|
||||
agent.Labels.Add(VarUtil.OS);
|
||||
agent.Labels.Add(VarUtil.OSArchitecture);
|
||||
agent.Labels.Clear();
|
||||
|
||||
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
|
||||
agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
|
||||
agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
|
||||
|
||||
foreach (var userLabel in userLabels)
|
||||
{
|
||||
agent.Labels.Add(new AgentLabel(userLabel, LabelType.User));
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey)
|
||||
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral)
|
||||
{
|
||||
TaskAgent agent = new TaskAgent(agentName)
|
||||
{
|
||||
@@ -489,45 +505,135 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
MaxParallelism = 1,
|
||||
Version = BuildConstants.RunnerPackage.Version,
|
||||
OSDescription = RuntimeInformation.OSDescription,
|
||||
Ephemeral = ephemeral,
|
||||
};
|
||||
|
||||
agent.Labels.Add("self-hosted");
|
||||
agent.Labels.Add(VarUtil.OS);
|
||||
agent.Labels.Add(VarUtil.OSArchitecture);
|
||||
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
|
||||
agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
|
||||
agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
|
||||
|
||||
foreach (var userLabel in userLabels)
|
||||
{
|
||||
agent.Labels.Add(new AgentLabel(userLabel, LabelType.User));
|
||||
}
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private async Task<bool> IsHostedServer(string serverUrl, VssCredentials credentials)
|
||||
private async Task<string> GetRunnerTokenAsync(CommandSettings command, string githubUrl, string tokenType)
|
||||
{
|
||||
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
|
||||
var locationServer = HostContext.GetService<ILocationServer>();
|
||||
VssConnection connection = VssUtil.CreateConnection(new Uri(serverUrl), credentials);
|
||||
await locationServer.ConnectAsync(connection);
|
||||
try
|
||||
var githubPAT = command.GetGitHubPersonalAccessToken();
|
||||
var runnerToken = string.Empty;
|
||||
if (!string.IsNullOrEmpty(githubPAT))
|
||||
{
|
||||
var connectionData = await locationServer.GetConnectionDataAsync();
|
||||
Trace.Info($"Server deployment type: {connectionData.DeploymentType}");
|
||||
return connectionData.DeploymentType.HasFlag(DeploymentFlags.Hosted);
|
||||
Trace.Info($"Retriving runner {tokenType} token using GitHub PAT.");
|
||||
var jitToken = await GetJITRunnerTokenAsync(githubUrl, githubPAT, tokenType);
|
||||
Trace.Info($"Retrived runner {tokenType} token is good to {jitToken.ExpiresAt}.");
|
||||
HostContext.SecretMasker.AddValue(jitToken.Token);
|
||||
runnerToken = jitToken.Token;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
if (string.IsNullOrEmpty(runnerToken))
|
||||
{
|
||||
// Since the DeploymentType is Enum, deserialization exception means there is a new Enum member been added.
|
||||
// It's more likely to be Hosted since OnPremises is always behind and customer can update their agent if are on-prem
|
||||
Trace.Error(ex);
|
||||
return true;
|
||||
if (string.Equals("registration", tokenType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
runnerToken = command.GetRunnerRegisterToken();
|
||||
}
|
||||
else
|
||||
{
|
||||
runnerToken = command.GetRunnerDeletionToken();
|
||||
}
|
||||
}
|
||||
|
||||
return runnerToken;
|
||||
}
|
||||
|
||||
private async Task<GitHubRunnerRegisterToken> GetJITRunnerTokenAsync(string githubUrl, string githubToken, string tokenType)
|
||||
{
|
||||
var githubApiUrl = "";
|
||||
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
||||
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (path.Length == 1)
|
||||
{
|
||||
// org runner
|
||||
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||
{
|
||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runners/{tokenType}-token";
|
||||
}
|
||||
else
|
||||
{
|
||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runners/{tokenType}-token";
|
||||
}
|
||||
}
|
||||
else if (path.Length == 2)
|
||||
{
|
||||
// repo or enterprise runner.
|
||||
var repoScope = "repos/";
|
||||
if (string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
repoScope = "";
|
||||
}
|
||||
|
||||
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||
{
|
||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{repoScope}{path[0]}/{path[1]}/actions/runners/{tokenType}-token";
|
||||
}
|
||||
else
|
||||
{
|
||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{repoScope}{path[0]}/{path[1]}/actions/runners/{tokenType}-token";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"'{githubUrl}' should point to an org or repository.");
|
||||
}
|
||||
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"github:{githubToken}"));
|
||||
HostContext.SecretMasker.AddValue(base64EncodingToken);
|
||||
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 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();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GitHubAuthResult> GetTenantCredential(string githubUrl, string githubToken, string runnerEvent)
|
||||
{
|
||||
var githubApiUrl = "";
|
||||
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
||||
var githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/actions/runner-registration";
|
||||
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||
{
|
||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/actions/runner-registration";
|
||||
}
|
||||
else
|
||||
{
|
||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/actions/runner-registration";
|
||||
}
|
||||
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(HostContext.UserAgent);
|
||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||
|
||||
var bodyObject = new Dictionary<string, string>()
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
public interface ICredentialManager : IRunnerService
|
||||
{
|
||||
ICredentialProvider GetCredentialProvider(string credType);
|
||||
VssCredentials LoadCredentials(bool preferMigrated = true);
|
||||
VssCredentials LoadCredentials();
|
||||
}
|
||||
|
||||
public class CredentialManager : RunnerService, ICredentialManager
|
||||
@@ -40,7 +40,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
return creds;
|
||||
}
|
||||
|
||||
public VssCredentials LoadCredentials(bool preferMigrated = true)
|
||||
public VssCredentials LoadCredentials()
|
||||
{
|
||||
IConfigurationStore store = HostContext.GetService<IConfigurationStore>();
|
||||
|
||||
@@ -50,14 +50,16 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
|
||||
CredentialData credData = store.GetCredentials();
|
||||
|
||||
if (preferMigrated)
|
||||
var migratedCred = store.GetMigratedCredentials();
|
||||
if (migratedCred != null)
|
||||
{
|
||||
var migratedCred = store.GetMigratedCredentials();
|
||||
if (migratedCred != null)
|
||||
{
|
||||
credData = migratedCred;
|
||||
}
|
||||
credData = migratedCred;
|
||||
|
||||
// Re-write .credentials with Token URL
|
||||
store.SaveCredential(credData);
|
||||
|
||||
// Delete .credentials_migrated
|
||||
store.DeleteMigratedCredential();
|
||||
}
|
||||
|
||||
ICredentialProvider credProv = GetCredentialProvider(credData.Scheme);
|
||||
@@ -69,6 +71,16 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public sealed class GitHubRunnerRegisterToken
|
||||
{
|
||||
[DataMember(Name = "token")]
|
||||
public string Token { get; set; }
|
||||
|
||||
[DataMember(Name = "expires_at")]
|
||||
public string ExpiresAt { get; set; }
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public sealed class GitHubAuthResult
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
/// key is returned to the caller.
|
||||
/// </summary>
|
||||
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the runner</returns>
|
||||
RSACryptoServiceProvider CreateKey();
|
||||
RSA CreateKey();
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the RSA key managed by the key manager.
|
||||
@@ -32,7 +32,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
/// </summary>
|
||||
/// <returns>An <c>RSACryptoServiceProvider</c> instance representing the key for the runner</returns>
|
||||
/// <exception cref="CryptographicException">No key exists in the store</exception>
|
||||
RSACryptoServiceProvider GetKey();
|
||||
RSA GetKey();
|
||||
}
|
||||
|
||||
// Newtonsoft 10 is not working properly with dotnet RSAParameters class
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
public string GetUniqueRunnerGroupName()
|
||||
{
|
||||
return RunnerServiceLocalGroupPrefix + IOUtil.GetPathHash(HostContext.GetDirectory(WellKnownDirectory.Bin)).Substring(0, 5);
|
||||
return RunnerServiceLocalGroupPrefix + IOUtil.GetSha256Hash(HostContext.GetDirectory(WellKnownDirectory.Bin)).Substring(0, 5);
|
||||
}
|
||||
|
||||
public bool LocalGroupExists(string groupName)
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
// We expect the key to be in the machine store at this point. Configuration should have set all of
|
||||
// this up correctly so we can use the key to generate access tokens.
|
||||
var keyManager = context.GetService<IRSAKeyManager>();
|
||||
var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey());
|
||||
var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey(), StringUtil.ConvertToBoolean(CredentialData.Data.GetValueOrDefault("requireFipsCryptography"), false));
|
||||
var clientCredential = new VssOAuthJwtBearerClientCredential(clientId, authorizationUrl, signingCredentials);
|
||||
var agentCredential = new VssOAuthCredential(new Uri(oauthEndpointUrl, UriKind.Absolute), VssOAuthGrant.ClientCredentials, clientCredential);
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
bool secret,
|
||||
string defaultValue,
|
||||
Func<String, bool> validator,
|
||||
bool unattended);
|
||||
bool unattended,
|
||||
bool isOptional = false);
|
||||
}
|
||||
|
||||
public sealed class PromptManager : RunnerService, IPromptManager
|
||||
@@ -56,7 +57,8 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
bool secret,
|
||||
string defaultValue,
|
||||
Func<string, bool> validator,
|
||||
bool unattended)
|
||||
bool unattended,
|
||||
bool isOptional = false)
|
||||
{
|
||||
Trace.Info(nameof(ReadValue));
|
||||
ArgUtil.NotNull(validator, nameof(validator));
|
||||
@@ -70,6 +72,10 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
else if (isOptional)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Otherwise throw.
|
||||
throw new Exception($"Invalid configuration provided for {argName}. Terminating unattended configuration.");
|
||||
@@ -79,24 +85,34 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
while (true)
|
||||
{
|
||||
// Write the message prompt.
|
||||
_terminal.Write($"{description} ", ConsoleColor.White);
|
||||
_terminal.Write($"{description} ");
|
||||
|
||||
if(!string.IsNullOrEmpty(defaultValue))
|
||||
{
|
||||
_terminal.Write($"[press Enter for {defaultValue}] ");
|
||||
}
|
||||
else if (isOptional){
|
||||
_terminal.Write($"[press Enter to skip] ");
|
||||
}
|
||||
|
||||
// Read and trim the value.
|
||||
value = secret ? _terminal.ReadSecret() : _terminal.ReadLine();
|
||||
value = value?.Trim() ?? string.Empty;
|
||||
|
||||
// Return the default if not specified.
|
||||
if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(defaultValue))
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
Trace.Info($"Falling back to the default: '{defaultValue}'");
|
||||
return defaultValue;
|
||||
if (!string.IsNullOrEmpty(defaultValue))
|
||||
{
|
||||
Trace.Info($"Falling back to the default: '{defaultValue}'");
|
||||
return defaultValue;
|
||||
}
|
||||
else if (isOptional)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Return the value if it is not empty and it is valid.
|
||||
// Otherwise try the loop again.
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
|
||||
@@ -13,14 +13,14 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
private string _keyFile;
|
||||
private IHostContext _context;
|
||||
|
||||
public RSACryptoServiceProvider CreateKey()
|
||||
public RSA CreateKey()
|
||||
{
|
||||
RSACryptoServiceProvider rsa = null;
|
||||
RSA rsa = null;
|
||||
if (!File.Exists(_keyFile))
|
||||
{
|
||||
Trace.Info("Creating new RSA key using 2048-bit key length");
|
||||
|
||||
rsa = new RSACryptoServiceProvider(2048);
|
||||
rsa = RSA.Create(2048);
|
||||
|
||||
// Now write the parameters to disk
|
||||
SaveParameters(rsa.ExportParameters(true));
|
||||
@@ -30,7 +30,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
Trace.Info("Found existing RSA key parameters file {0}", _keyFile);
|
||||
|
||||
rsa = new RSACryptoServiceProvider();
|
||||
rsa = RSA.Create();
|
||||
rsa.ImportParameters(LoadParameters());
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public RSACryptoServiceProvider GetKey()
|
||||
public RSA GetKey()
|
||||
{
|
||||
if (!File.Exists(_keyFile))
|
||||
{
|
||||
@@ -55,7 +55,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
|
||||
Trace.Info("Loading RSA key parameters from file {0}", _keyFile);
|
||||
|
||||
var rsa = new RSACryptoServiceProvider();
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportParameters(LoadParameters());
|
||||
return rsa;
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
private string _keyFile;
|
||||
private IHostContext _context;
|
||||
|
||||
public RSACryptoServiceProvider CreateKey()
|
||||
public RSA CreateKey()
|
||||
{
|
||||
RSACryptoServiceProvider rsa = null;
|
||||
RSA rsa = null;
|
||||
if (!File.Exists(_keyFile))
|
||||
{
|
||||
Trace.Info("Creating new RSA key using 2048-bit key length");
|
||||
|
||||
rsa = new RSACryptoServiceProvider(2048);
|
||||
rsa = RSA.Create(2048);
|
||||
|
||||
// Now write the parameters to disk
|
||||
IOUtil.SaveObject(new RSAParametersSerializable(rsa.ExportParameters(true)), _keyFile);
|
||||
@@ -54,7 +54,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
Trace.Info("Found existing RSA key parameters file {0}", _keyFile);
|
||||
|
||||
rsa = new RSACryptoServiceProvider();
|
||||
rsa = RSA.Create();
|
||||
rsa.ImportParameters(IOUtil.LoadObject<RSAParametersSerializable>(_keyFile).RSAParameters);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
public RSACryptoServiceProvider GetKey()
|
||||
public RSA GetKey()
|
||||
{
|
||||
if (!File.Exists(_keyFile))
|
||||
{
|
||||
@@ -80,7 +80,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
Trace.Info("Loading RSA key parameters from file {0}", _keyFile);
|
||||
|
||||
var parameters = IOUtil.LoadObject<RSAParametersSerializable>(_keyFile).RSAParameters;
|
||||
var rsa = new RSACryptoServiceProvider();
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportParameters(parameters);
|
||||
return rsa;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Security.Principal;
|
||||
|
||||
@@ -46,6 +47,21 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
string.Equals(value, "N", StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool LabelsValidator(string labels)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(labels))
|
||||
{
|
||||
var labelSet = labels.Split(',').Where(x => !string.IsNullOrEmpty(x)).ToHashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (labelSet.Any(x => x.Length > 256))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool NonEmptyValidator(string value)
|
||||
{
|
||||
return !string.IsNullOrEmpty(value);
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Linq;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.WebApi.Jwt;
|
||||
|
||||
namespace GitHub.Runner.Listener
|
||||
{
|
||||
@@ -26,24 +27,27 @@ namespace GitHub.Runner.Listener
|
||||
Task ShutdownAsync();
|
||||
}
|
||||
|
||||
// This implementation of IDobDispatcher is not thread safe.
|
||||
// It is base on the fact that the current design of runner is dequeue
|
||||
// and process one message from message queue everytime.
|
||||
// In addition, it only execute one job every time,
|
||||
// and server will not send another job while this one is still running.
|
||||
// This implementation of IJobDispatcher is not thread safe.
|
||||
// It is based on the fact that the current design of the runner is a dequeue
|
||||
// and processes one message from the message queue at a time.
|
||||
// In addition, it only executes one job every time,
|
||||
// and the server will not send another job while this one is still running.
|
||||
public sealed class JobDispatcher : RunnerService, IJobDispatcher
|
||||
{
|
||||
private readonly Lazy<Dictionary<long, TaskResult>> _localRunJobResult = new Lazy<Dictionary<long, TaskResult>>();
|
||||
private int _poolId;
|
||||
RunnerSettings _runnerSetting;
|
||||
|
||||
IConfigurationStore _configurationStore;
|
||||
|
||||
RunnerSettings _runnerSettings;
|
||||
private static readonly string _workerProcessName = $"Runner.Worker{IOUtil.ExeExtension}";
|
||||
|
||||
// this is not thread-safe
|
||||
private readonly Queue<Guid> _jobDispatchedQueue = new Queue<Guid>();
|
||||
private readonly ConcurrentDictionary<Guid, WorkerDispatcher> _jobInfos = new ConcurrentDictionary<Guid, WorkerDispatcher>();
|
||||
|
||||
//allow up to 30sec for any data to be transmitted over the process channel
|
||||
//timeout limit can be overwrite by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT
|
||||
// allow up to 30sec for any data to be transmitted over the process channel
|
||||
// timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT
|
||||
private TimeSpan _channelTimeout;
|
||||
|
||||
private TaskCompletionSource<bool> _runOnceJobCompleted = new TaskCompletionSource<bool>();
|
||||
@@ -53,9 +57,9 @@ namespace GitHub.Runner.Listener
|
||||
base.Initialize(hostContext);
|
||||
|
||||
// get pool id from config
|
||||
var configurationStore = hostContext.GetService<IConfigurationStore>();
|
||||
_runnerSetting = configurationStore.GetSettings();
|
||||
_poolId = _runnerSetting.PoolId;
|
||||
_configurationStore = hostContext.GetService<IConfigurationStore>();
|
||||
_runnerSettings = _configurationStore.GetSettings();
|
||||
_poolId = _runnerSettings.PoolId;
|
||||
|
||||
int channelTimeoutSeconds;
|
||||
if (!int.TryParse(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT") ?? string.Empty, out channelTimeoutSeconds))
|
||||
@@ -63,7 +67,7 @@ namespace GitHub.Runner.Listener
|
||||
channelTimeoutSeconds = 30;
|
||||
}
|
||||
|
||||
// _channelTimeout should in range [30, 300] seconds
|
||||
// _channelTimeout should be in range [30, 300] seconds
|
||||
_channelTimeout = TimeSpan.FromSeconds(Math.Min(Math.Max(channelTimeoutSeconds, 30), 300));
|
||||
Trace.Info($"Set runner/worker IPC timeout to {_channelTimeout.TotalSeconds} seconds.");
|
||||
}
|
||||
@@ -86,15 +90,30 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
var orchestrationId = string.Empty;
|
||||
var systemConnection = jobRequestMessage.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
if (systemConnection?.Authorization != null &&
|
||||
systemConnection.Authorization.Parameters.TryGetValue("AccessToken", out var accessToken) &&
|
||||
!string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
var jwt = JsonWebToken.Create(accessToken);
|
||||
var claims = jwt.ExtractClaims();
|
||||
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
|
||||
if (!string.IsNullOrEmpty(orchestrationId))
|
||||
{
|
||||
Trace.Info($"Pull OrchestrationId {orchestrationId} from JWT claims");
|
||||
}
|
||||
}
|
||||
|
||||
WorkerDispatcher newDispatch = new WorkerDispatcher(jobRequestMessage.JobId, jobRequestMessage.RequestId);
|
||||
if (runOnce)
|
||||
{
|
||||
Trace.Info("Start dispatcher for one time used runner.");
|
||||
newDispatch.WorkerDispatch = RunOnceAsync(jobRequestMessage, currentDispatch, newDispatch.WorkerCancellationTokenSource.Token, newDispatch.WorkerCancelTimeoutKillTokenSource.Token);
|
||||
newDispatch.WorkerDispatch = RunOnceAsync(jobRequestMessage, orchestrationId, currentDispatch, newDispatch.WorkerCancellationTokenSource.Token, newDispatch.WorkerCancelTimeoutKillTokenSource.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
newDispatch.WorkerDispatch = RunAsync(jobRequestMessage, currentDispatch, newDispatch.WorkerCancellationTokenSource.Token, newDispatch.WorkerCancelTimeoutKillTokenSource.Token);
|
||||
newDispatch.WorkerDispatch = RunAsync(jobRequestMessage, orchestrationId, currentDispatch, newDispatch.WorkerCancellationTokenSource.Token, newDispatch.WorkerCancelTimeoutKillTokenSource.Token);
|
||||
}
|
||||
|
||||
_jobInfos.TryAdd(newDispatch.JobId, newDispatch);
|
||||
@@ -214,16 +233,27 @@ namespace GitHub.Runner.Listener
|
||||
return;
|
||||
}
|
||||
|
||||
// base on the current design, server will only send one job for a given runner everytime.
|
||||
// if the runner received a new job request while a previous job request is still running, this typically indicate two situations
|
||||
// 1. an runner bug cause server and runner mismatch on the state of the job request, ex. runner not renew jobrequest properly but think it still own the job reqest, however server already abandon the jobrequest.
|
||||
// 2. a server bug or design change that allow server send more than one job request to an given runner that haven't finish previous job request.
|
||||
// based on the current design, server will only send one job for a given runner at a time.
|
||||
// if the runner received a new job request while a previous job request is still running, this typically indicates two situations
|
||||
// 1. a runner bug caused a server and runner mismatch on the state of the job request, e.g. the runner didn't renew the jobrequest
|
||||
// properly but thinks it still owns the job reqest, however the server has already abandoned the jobrequest.
|
||||
// 2. a server bug or design change that allowed the server to send more than one job request to an given runner that hasn't finished
|
||||
//. a previous job request.
|
||||
var runnerServer = HostContext.GetService<IRunnerServer>();
|
||||
TaskAgentJobRequest request = null;
|
||||
try
|
||||
{
|
||||
request = await runnerServer.GetAgentRequestAsync(_poolId, jobDispatch.RequestId, CancellationToken.None);
|
||||
}
|
||||
catch (TaskAgentJobNotFoundException ex)
|
||||
{
|
||||
Trace.Error($"Catch job-not-found exception while checking jobrequest {jobDispatch.JobId} status. Cancel running worker right away.");
|
||||
Trace.Error(ex);
|
||||
jobDispatch.WorkerCancellationTokenSource.Cancel();
|
||||
// make sure worker process exits before we return, otherwise we might leave an orphan worker process behind.
|
||||
await jobDispatch.WorkerDispatch;
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// we can't even query for the jobrequest from server, something totally busted, stop runner/worker.
|
||||
@@ -231,7 +261,7 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Error(ex);
|
||||
|
||||
jobDispatch.WorkerCancellationTokenSource.Cancel();
|
||||
// make sure worker process exit before we rethrow, otherwise we might leave orphan worker process behind.
|
||||
// make sure the worker process exits before we rethrow, otherwise we might leave orphan worker process behind.
|
||||
await jobDispatch.WorkerDispatch;
|
||||
|
||||
// rethrow original exception
|
||||
@@ -240,8 +270,8 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
if (request.Result != null)
|
||||
{
|
||||
// job request has been finished, the server already has result.
|
||||
// this means runner is busted since it still running that request.
|
||||
// job request has been finished, the server already has the result.
|
||||
// this means the runner is busted since it is still running that request.
|
||||
// cancel the zombie worker, run next job request.
|
||||
Trace.Error($"Received job request while previous job {jobDispatch.JobId} still running on worker. Cancel the previous job since the job request have been finished on server side with result: {request.Result.Value}.");
|
||||
jobDispatch.WorkerCancellationTokenSource.Cancel();
|
||||
@@ -284,11 +314,11 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||
private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await RunAsync(message, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
|
||||
await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -297,7 +327,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAsync(Pipelines.AgentJobRequestMessage message, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||
private async Task RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||
{
|
||||
Busy = true;
|
||||
try
|
||||
@@ -328,7 +358,7 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
// start renew job request
|
||||
Trace.Info($"Start renew job request {requestId} for job {message.JobId}.");
|
||||
Task renewJobRequest = RenewJobRequestAsync(_poolId, requestId, lockToken, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
||||
Task renewJobRequest = RenewJobRequestAsync(_poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
||||
|
||||
// wait till first renew succeed or job request is canceled
|
||||
// not even start worker if the first renew fail
|
||||
@@ -480,7 +510,20 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
detailInfo = string.Join(Environment.NewLine, workerOutput);
|
||||
Trace.Info($"Return code {returnCode} indicate worker encounter an unhandled exception or app crash, attach worker stdout/stderr to JobRequest result.");
|
||||
await LogWorkerProcessUnhandledException(message, detailInfo);
|
||||
|
||||
var jobServer = HostContext.GetService<IJobServer>();
|
||||
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
||||
VssConnection jobConnection = VssUtil.CreateConnection(systemConnection.Url, jobServerCredential);
|
||||
await jobServer.ConnectAsync(jobConnection);
|
||||
|
||||
await LogWorkerProcessUnhandledException(jobServer, message, detailInfo);
|
||||
|
||||
// Go ahead to finish the job with result 'Failed' if the STDERR from worker is System.IO.IOException, since it typically means we are running out of disk space.
|
||||
if (detailInfo.Contains(typeof(System.IO.IOException).ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Trace.Info($"Finish job with result 'Failed' due to IOException.");
|
||||
await ForceFailJob(jobServer, message);
|
||||
}
|
||||
}
|
||||
|
||||
TaskResult result = TaskResultUtil.TranslateFromReturnCode(returnCode);
|
||||
@@ -607,7 +650,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RenewJobRequestAsync(int poolId, long requestId, Guid lockToken, TaskCompletionSource<int> firstJobRequestRenewed, CancellationToken token)
|
||||
public async Task RenewJobRequestAsync(int poolId, long requestId, Guid lockToken, string orchestrationId, TaskCompletionSource<int> firstJobRequestRenewed, CancellationToken token)
|
||||
{
|
||||
var runnerServer = HostContext.GetService<IRunnerServer>();
|
||||
TaskAgentJobRequest request = null;
|
||||
@@ -620,14 +663,16 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
try
|
||||
{
|
||||
request = await runnerServer.RenewAgentRequestAsync(poolId, requestId, lockToken, token);
|
||||
|
||||
request = await runnerServer.RenewAgentRequestAsync(poolId, requestId, lockToken, orchestrationId, token);
|
||||
Trace.Info($"Successfully renew job request {requestId}, job is valid till {request.LockedUntil.Value}");
|
||||
|
||||
if (!firstJobRequestRenewed.Task.IsCompleted)
|
||||
{
|
||||
// fire first renew succeed event.
|
||||
firstJobRequestRenewed.TrySetResult(0);
|
||||
|
||||
// Update settings if the runner name has been changed server-side
|
||||
UpdateAgentNameIfNeeded(request.ReservedAgent?.Name);
|
||||
}
|
||||
|
||||
if (encounteringError > 0)
|
||||
@@ -727,6 +772,27 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAgentNameIfNeeded(string agentName)
|
||||
{
|
||||
var isNewAgentName = !string.Equals(_runnerSettings.AgentName, agentName, StringComparison.Ordinal);
|
||||
if (!isNewAgentName || string.IsNullOrEmpty(agentName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runnerSettings.AgentName = agentName;
|
||||
try
|
||||
{
|
||||
_configurationStore.SaveSettings(_runnerSettings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Cannot update the settings file:");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Best effort upload any logs for this job.
|
||||
private async Task TryUploadUnfinishedLogs(Pipelines.AgentJobRequestMessage message)
|
||||
{
|
||||
@@ -842,7 +908,6 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We need send detailInfo back to DT in order to add an issue for the job
|
||||
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, Guid lockToken, TaskResult result, string detailInfo = null)
|
||||
{
|
||||
Trace.Entering();
|
||||
@@ -889,55 +954,20 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
|
||||
// log an error issue to job level timeline record
|
||||
private async Task LogWorkerProcessUnhandledException(Pipelines.AgentJobRequestMessage message, string errorMessage)
|
||||
private async Task LogWorkerProcessUnhandledException(IJobServer jobServer, Pipelines.AgentJobRequestMessage message, string errorMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection));
|
||||
ArgUtil.NotNull(systemConnection, nameof(systemConnection));
|
||||
|
||||
var jobServer = HostContext.GetService<IJobServer>();
|
||||
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
||||
VssConnection jobConnection = VssUtil.CreateConnection(systemConnection.Url, jobServerCredential);
|
||||
|
||||
/* Below is the legacy 'OnPremises' code that is currently unused by the runner
|
||||
ToDo: re-implement code as appropriate once GHES support is added.
|
||||
// Make sure SystemConnection Url match Config Url base for OnPremises server
|
||||
if (!message.Variables.ContainsKey(Constants.Variables.System.ServerType) ||
|
||||
string.Equals(message.Variables[Constants.Variables.System.ServerType]?.Value, "OnPremises", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri result = null;
|
||||
Uri configUri = new Uri(_runnerSetting.ServerUrl);
|
||||
if (Uri.TryCreate(new Uri(configUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)), jobServerUrl.PathAndQuery, out result))
|
||||
{
|
||||
//replace the schema and host portion of messageUri with the host from the
|
||||
//server URI (which was set at config time)
|
||||
jobServerUrl = result;
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
//cannot parse the Uri - not a fatal error
|
||||
Trace.Error(ex);
|
||||
}
|
||||
catch (UriFormatException ex)
|
||||
{
|
||||
//cannot parse the Uri - not a fatal error
|
||||
Trace.Error(ex);
|
||||
}
|
||||
} */
|
||||
|
||||
await jobServer.ConnectAsync(jobConnection);
|
||||
|
||||
var timeline = await jobServer.GetTimelineAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, CancellationToken.None);
|
||||
|
||||
ArgUtil.NotNull(timeline, nameof(timeline));
|
||||
|
||||
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
||||
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
||||
|
||||
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = errorMessage };
|
||||
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
||||
jobRecord.ErrorCount++;
|
||||
jobRecord.Issues.Add(new Issue() { Type = IssueType.Error, Message = errorMessage });
|
||||
jobRecord.Issues.Add(unhandledExceptionIssue);
|
||||
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -947,6 +977,21 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
// raise job completed event to fail the job.
|
||||
private async Task ForceFailJob(IJobServer jobServer, Pipelines.AgentJobRequestMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jobCompletedEvent = new JobCompletedEvent(message.RequestId, message.JobId, TaskResult.Failed);
|
||||
await jobServer.RaisePlanEventAsync<JobCompletedEvent>(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, jobCompletedEvent, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to raise JobCompletedEvent back to service.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private class WorkerDispatcher : IDisposable
|
||||
{
|
||||
public long RequestId { get; }
|
||||
|
||||
@@ -13,10 +13,7 @@ using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.WebApi;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Test")]
|
||||
namespace GitHub.Runner.Listener
|
||||
{
|
||||
[ServiceLocator(Default = typeof(MessageListener))]
|
||||
@@ -35,30 +32,18 @@ namespace GitHub.Runner.Listener
|
||||
private ITerminal _term;
|
||||
private IRunnerServer _runnerServer;
|
||||
private TaskAgentSession _session;
|
||||
private ICredentialManager _credMgr;
|
||||
private IConfigurationStore _configStore;
|
||||
private TimeSpan _getNextMessageRetryInterval;
|
||||
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
|
||||
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
|
||||
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
|
||||
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new Dictionary<string, int>();
|
||||
|
||||
// Whether load credentials from .credentials_migrated file
|
||||
internal bool _useMigratedCredentials;
|
||||
|
||||
// need to check auth url if there is only .credentials and auth schema is OAuth
|
||||
internal bool _needToCheckAuthorizationUrlUpdate;
|
||||
internal Task<VssCredentials> _authorizationUrlMigrationBackgroundTask;
|
||||
internal Task _authorizationUrlRollbackReattemptDelayBackgroundTask;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
|
||||
_term = HostContext.GetService<ITerminal>();
|
||||
_runnerServer = HostContext.GetService<IRunnerServer>();
|
||||
_credMgr = HostContext.GetService<ICredentialManager>();
|
||||
_configStore = HostContext.GetService<IConfigurationStore>();
|
||||
}
|
||||
|
||||
public async Task<Boolean> CreateSessionAsync(CancellationToken token)
|
||||
@@ -73,8 +58,8 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
// Create connection.
|
||||
Trace.Info("Loading Credentials");
|
||||
_useMigratedCredentials = !StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_SPSAUTHURL"));
|
||||
VssCredentials creds = _credMgr.LoadCredentials(_useMigratedCredentials);
|
||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||
VssCredentials creds = credMgr.LoadCredentials();
|
||||
|
||||
var agent = new TaskAgentReference
|
||||
{
|
||||
@@ -89,17 +74,6 @@ namespace GitHub.Runner.Listener
|
||||
string errorMessage = string.Empty;
|
||||
bool encounteringError = false;
|
||||
|
||||
var originalCreds = _configStore.GetCredentials();
|
||||
var migratedCreds = _configStore.GetMigratedCredentials();
|
||||
if (migratedCreds == null)
|
||||
{
|
||||
_useMigratedCredentials = false;
|
||||
if (originalCreds.Scheme == Constants.Configuration.OAuth)
|
||||
{
|
||||
_needToCheckAuthorizationUrlUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -127,12 +101,6 @@ namespace GitHub.Runner.Listener
|
||||
encounteringError = false;
|
||||
}
|
||||
|
||||
if (_needToCheckAuthorizationUrlUpdate)
|
||||
{
|
||||
// start background task try to get new authorization url
|
||||
_authorizationUrlMigrationBackgroundTask = GetNewOAuthAuthorizationSetting(token);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
@@ -150,25 +118,26 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Error("Catch exception during create session.");
|
||||
Trace.Error(ex);
|
||||
|
||||
if (!IsSessionCreationExceptionRetriable(ex))
|
||||
if (ex is VssOAuthTokenRequestException && creds.Federated is VssOAuthCredential vssOAuthCred)
|
||||
{
|
||||
if (_useMigratedCredentials)
|
||||
// Check whether we get 401 because the runner registration already removed by the service.
|
||||
// If the runner registration get deleted, we can't exchange oauth token.
|
||||
Trace.Error("Test oauth app registration.");
|
||||
var oauthTokenProvider = new VssOAuthTokenProvider(vssOAuthCred, new Uri(serverUrl));
|
||||
var authError = await oauthTokenProvider.ValidateCredentialAsync(token);
|
||||
if (string.Equals(authError, "invalid_client", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// migrated credentials might cause lose permission during permission check,
|
||||
// we will force to use original credential and try again
|
||||
_useMigratedCredentials = false;
|
||||
var reattemptBackoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromHours(24), TimeSpan.FromHours(36));
|
||||
_authorizationUrlRollbackReattemptDelayBackgroundTask = HostContext.Delay(reattemptBackoff, token); // retry migrated creds in 24-36 hours.
|
||||
creds = _credMgr.LoadCredentials(false);
|
||||
Trace.Error("Fallback to original credentials and try again.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_term.WriteError($"Failed to create session. {ex.Message}");
|
||||
_term.WriteError("Failed to create a session. The runner registration has been deleted from the server, please re-configure.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsSessionCreationExceptionRetriable(ex))
|
||||
{
|
||||
_term.WriteError($"Failed to create session. {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!encounteringError) //print the message only on the first error
|
||||
{
|
||||
_term.WriteError($"{DateTime.UtcNow:u}: Runner connect error: {ex.Message}. Retrying until reconnected.");
|
||||
@@ -227,51 +196,6 @@ namespace GitHub.Runner.Listener
|
||||
encounteringError = false;
|
||||
continuousError = 0;
|
||||
}
|
||||
|
||||
if (_needToCheckAuthorizationUrlUpdate &&
|
||||
_authorizationUrlMigrationBackgroundTask?.IsCompleted == true)
|
||||
{
|
||||
if (HostContext.GetService<IJobDispatcher>().Busy ||
|
||||
HostContext.GetService<ISelfUpdater>().Busy)
|
||||
{
|
||||
Trace.Info("Job or runner updates in progress, update credentials next time.");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var newCred = await _authorizationUrlMigrationBackgroundTask;
|
||||
await _runnerServer.ConnectAsync(new Uri(_settings.ServerUrl), newCred);
|
||||
Trace.Info("Updated connection to use migrated credential for next GetMessage call.");
|
||||
_useMigratedCredentials = true;
|
||||
_authorizationUrlMigrationBackgroundTask = null;
|
||||
_needToCheckAuthorizationUrlUpdate = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to refresh connection with new authorization url.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_authorizationUrlRollbackReattemptDelayBackgroundTask?.IsCompleted == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
// we rolled back to use original creds about 2 days before, now it's a good time to try migrated creds again.
|
||||
Trace.Info("Re-attempt to use migrated credential");
|
||||
var migratedCreds = _credMgr.LoadCredentials();
|
||||
await _runnerServer.ConnectAsync(new Uri(_settings.ServerUrl), migratedCreds);
|
||||
_useMigratedCredentials = true;
|
||||
_authorizationUrlRollbackReattemptDelayBackgroundTask = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to refresh connection with new authorization url on rollback reattempt.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -295,21 +219,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
else if (!IsGetNextMessageExceptionRetriable(ex))
|
||||
{
|
||||
if (_useMigratedCredentials)
|
||||
{
|
||||
// migrated credentials might cause lose permission during permission check,
|
||||
// we will force to use original credential and try again
|
||||
_useMigratedCredentials = false;
|
||||
var reattemptBackoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromHours(24), TimeSpan.FromHours(36));
|
||||
_authorizationUrlRollbackReattemptDelayBackgroundTask = HostContext.Delay(reattemptBackoff, token); // retry migrated creds in 24-36 hours.
|
||||
var originalCreds = _credMgr.LoadCredentials(false);
|
||||
await _runnerServer.ConnectAsync(new Uri(_settings.ServerUrl), originalCreds);
|
||||
Trace.Error("Fallback to original credentials and try again.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -409,7 +319,8 @@ namespace GitHub.Runner.Listener
|
||||
var keyManager = HostContext.GetService<IRSAKeyManager>();
|
||||
using (var rsa = keyManager.GetKey())
|
||||
{
|
||||
return aes.CreateDecryptor(rsa.Decrypt(_session.EncryptionKey.Value, RSAEncryptionPadding.OaepSHA1), message.IV);
|
||||
var padding = _session.UseFipsEncryption ? RSAEncryptionPadding.OaepSHA256 : RSAEncryptionPadding.OaepSHA1;
|
||||
return aes.CreateDecryptor(rsa.Decrypt(_session.EncryptionKey.Value, padding), message.IV);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -501,80 +412,5 @@ namespace GitHub.Runner.Listener
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<VssCredentials> GetNewOAuthAuthorizationSetting(CancellationToken token)
|
||||
{
|
||||
Trace.Info("Start checking oauth authorization url update.");
|
||||
while (true)
|
||||
{
|
||||
var backoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(45));
|
||||
await HostContext.Delay(backoff, token);
|
||||
|
||||
try
|
||||
{
|
||||
var migratedAuthorizationUrl = await _runnerServer.GetRunnerAuthUrlAsync(_settings.PoolId, _settings.AgentId);
|
||||
if (!string.IsNullOrEmpty(migratedAuthorizationUrl))
|
||||
{
|
||||
var credData = _configStore.GetCredentials();
|
||||
var clientId = credData.Data.GetValueOrDefault("clientId", null);
|
||||
var currentAuthorizationUrl = credData.Data.GetValueOrDefault("authorizationUrl", null);
|
||||
Trace.Info($"Current authorization url: {currentAuthorizationUrl}, new authorization url: {migratedAuthorizationUrl}");
|
||||
|
||||
if (string.Equals(currentAuthorizationUrl, migratedAuthorizationUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// We don't need to update credentials.
|
||||
Trace.Info("No needs to update authorization url");
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(-1), token);
|
||||
}
|
||||
|
||||
var keyManager = HostContext.GetService<IRSAKeyManager>();
|
||||
var signingCredentials = VssSigningCredentials.Create(() => keyManager.GetKey());
|
||||
|
||||
var migratedClientCredential = new VssOAuthJwtBearerClientCredential(clientId, migratedAuthorizationUrl, signingCredentials);
|
||||
var migratedRunnerCredential = new VssOAuthCredential(new Uri(migratedAuthorizationUrl, UriKind.Absolute), VssOAuthGrant.ClientCredentials, migratedClientCredential);
|
||||
|
||||
Trace.Info("Try connect service with Token Service OAuth endpoint.");
|
||||
var runnerServer = HostContext.CreateService<IRunnerServer>();
|
||||
await runnerServer.ConnectAsync(new Uri(_settings.ServerUrl), migratedRunnerCredential);
|
||||
await runnerServer.GetAgentPoolsAsync();
|
||||
Trace.Info($"Successfully connected service with new authorization url.");
|
||||
|
||||
var migratedCredData = new CredentialData
|
||||
{
|
||||
Scheme = Constants.Configuration.OAuth,
|
||||
Data =
|
||||
{
|
||||
{ "clientId", clientId },
|
||||
{ "authorizationUrl", migratedAuthorizationUrl },
|
||||
{ "oauthEndpointUrl", migratedAuthorizationUrl },
|
||||
},
|
||||
};
|
||||
|
||||
_configStore.SaveMigratedCredential(migratedCredData);
|
||||
return migratedRunnerCredential;
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Verbose("No authorization url updates");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to get/test new authorization url.");
|
||||
Trace.Error(ex);
|
||||
|
||||
try
|
||||
{
|
||||
await _runnerServer.ReportRunnerAuthUrlErrorAsync(_settings.PoolId, _settings.AgentId, ex.ToString());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// best effort
|
||||
Trace.Error("Fail to report the migration error");
|
||||
Trace.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,9 @@ namespace GitHub.Runner.Listener
|
||||
IRunner runner = context.GetService<IRunner>();
|
||||
try
|
||||
{
|
||||
return await runner.ExecuteCommand(command);
|
||||
var returnCode = await runner.ExecuteCommand(command);
|
||||
trace.Info($"Runner execution has finished with return code {returnCode}");
|
||||
return returnCode;
|
||||
}
|
||||
catch (OperationCanceledException) when (context.RunnerShutdownToken.IsCancellationRequested)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<AssetTargetFallback>portable-net45+win8</AssetTargetFallback>
|
||||
<NoWarn>NU1701;NU1603</NoWarn>
|
||||
<Version>$(Version)</Version>
|
||||
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Listener.Configuration;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -11,6 +10,8 @@ using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Linq;
|
||||
using GitHub.Runner.Listener.Check;
|
||||
|
||||
namespace GitHub.Runner.Listener
|
||||
{
|
||||
@@ -37,7 +38,7 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
try
|
||||
{
|
||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgent, HostContext.WebProxy);
|
||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
|
||||
|
||||
_inConfigStage = true;
|
||||
_completedCommand.Reset();
|
||||
@@ -72,6 +73,46 @@ namespace GitHub.Runner.Listener
|
||||
return Constants.Runner.ReturnCode.Success;
|
||||
}
|
||||
|
||||
if (command.Check)
|
||||
{
|
||||
var url = command.GetUrl();
|
||||
var pat = command.GetGitHubPersonalAccessToken(required: true);
|
||||
var checkExtensions = HostContext.GetService<IExtensionManager>().GetExtensions<ICheckExtension>();
|
||||
var sortedChecks = checkExtensions.OrderBy(x => x.Order);
|
||||
foreach (var check in sortedChecks)
|
||||
{
|
||||
_term.WriteLine($"**********************************************************************************************************************");
|
||||
_term.WriteLine($"** Check: {check.CheckName}");
|
||||
_term.WriteLine($"** Description: {check.CheckDescription}");
|
||||
_term.WriteLine($"**********************************************************************************************************************");
|
||||
var result = await check.RunCheck(url, pat);
|
||||
if (!result)
|
||||
{
|
||||
_term.WriteLine($"** **");
|
||||
_term.WriteLine($"** F A I L **");
|
||||
_term.WriteLine($"** **");
|
||||
_term.WriteLine($"**********************************************************************************************************************");
|
||||
_term.WriteLine($"** Log: {check.CheckLog}");
|
||||
_term.WriteLine($"** Help Doc: {check.HelpLink}");
|
||||
_term.WriteLine($"**********************************************************************************************************************");
|
||||
}
|
||||
else
|
||||
{
|
||||
_term.WriteLine($"** **");
|
||||
_term.WriteLine($"** P A S S **");
|
||||
_term.WriteLine($"** **");
|
||||
_term.WriteLine($"**********************************************************************************************************************");
|
||||
_term.WriteLine($"** Log: {check.CheckLog}");
|
||||
_term.WriteLine($"**********************************************************************************************************************");
|
||||
}
|
||||
|
||||
_term.WriteLine();
|
||||
_term.WriteLine();
|
||||
}
|
||||
|
||||
return Constants.Runner.ReturnCode.Success;
|
||||
}
|
||||
|
||||
// Configure runner prompt for args if not supplied
|
||||
// Unattended configure mode will not prompt for args if not supplied and error on any missing or invalid value.
|
||||
if (command.Configure)
|
||||
@@ -173,7 +214,7 @@ namespace GitHub.Runner.Listener
|
||||
var startupTypeAsString = command.GetStartupType();
|
||||
if (string.IsNullOrEmpty(startupTypeAsString) && configuredAsService)
|
||||
{
|
||||
// We need try our best to make the startup type accurate
|
||||
// We need try our best to make the startup type accurate
|
||||
// The problem is coming from runner autoupgrade, which result an old version service host binary but a newer version runner binary
|
||||
// At that time the servicehost won't pass --startuptype to Runner.Listener while the runner is actually running as service.
|
||||
// We will guess the startup type only when the runner is configured as service and the guess will based on whether STDOUT/STDERR/STDIN been redirect or not
|
||||
@@ -192,8 +233,14 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Info($"Set runner startup type - {startType}");
|
||||
HostContext.StartupType = startType;
|
||||
|
||||
if (command.RunOnce)
|
||||
{
|
||||
_term.WriteLine("Warning: '--once' is going to be deprecated in the future, please consider using '--ephemeral' during runner registration.", ConsoleColor.Yellow);
|
||||
_term.WriteLine("https://docs.github.com/en/actions/hosting-your-own-runners/autoscaling-with-self-hosted-runners#using-ephemeral-runners-for-autoscaling", ConsoleColor.Yellow);
|
||||
}
|
||||
|
||||
// Run the runner interactively or as service
|
||||
return await RunAsync(settings, command.RunOnce);
|
||||
return await RunAsync(settings, command.RunOnce || settings.Ephemeral);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -265,10 +312,15 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
|
||||
HostContext.WritePerfCounter("SessionCreated");
|
||||
|
||||
_term.WriteLine($"Current runner version: '{BuildConstants.RunnerPackage.Version}'");
|
||||
_term.WriteLine($"{DateTime.UtcNow:u}: Listening for Jobs");
|
||||
|
||||
IJobDispatcher jobDispatcher = null;
|
||||
CancellationTokenSource messageQueueLoopTokenSource = CancellationTokenSource.CreateLinkedTokenSource(HostContext.RunnerShutdownToken);
|
||||
|
||||
// Should we try to cleanup ephemeral runners
|
||||
bool runOnceJobCompleted = false;
|
||||
try
|
||||
{
|
||||
var notification = HostContext.GetService<IJobNotification>();
|
||||
@@ -330,6 +382,7 @@ namespace GitHub.Runner.Listener
|
||||
Task completeTask = await Task.WhenAny(getNextMessage, jobDispatcher.RunOnceJobCompleted.Task);
|
||||
if (completeTask == jobDispatcher.RunOnceJobCompleted.Task)
|
||||
{
|
||||
runOnceJobCompleted = true;
|
||||
Trace.Info("Job has finished at backend, the runner will exit since it is running under onetime use mode.");
|
||||
Trace.Info("Stop message queue looping.");
|
||||
messageQueueLoopTokenSource.Cancel();
|
||||
@@ -425,10 +478,24 @@ namespace GitHub.Runner.Listener
|
||||
await jobDispatcher.ShutdownAsync();
|
||||
}
|
||||
|
||||
//TODO: make sure we don't mask more important exception
|
||||
await _listener.DeleteSessionAsync();
|
||||
try
|
||||
{
|
||||
await _listener.DeleteSessionAsync();
|
||||
}
|
||||
catch (Exception ex) when (runOnce)
|
||||
{
|
||||
// ignore exception during delete session for ephemeral runner since the runner might already be deleted from the server side
|
||||
// and the delete session call will ends up with 401.
|
||||
Trace.Info($"Ignore any exception during DeleteSession for an ephemeral runner. {ex}");
|
||||
}
|
||||
|
||||
messageQueueLoopTokenSource.Dispose();
|
||||
|
||||
if (settings.Ephemeral && runOnceJobCompleted)
|
||||
{
|
||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||
configManager.DeleteLocalRunnerConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskAgentAccessTokenExpiredException)
|
||||
@@ -460,14 +527,20 @@ Options:
|
||||
--help Prints the help for each command
|
||||
--version Prints the runner version
|
||||
--commit Prints the runner commit
|
||||
--check Check the runner's network connectivity with GitHub server
|
||||
|
||||
Config Options:
|
||||
--unattended Disable interactive prompts for missing arguments. Defaults will be used for missing options
|
||||
--url string Repository to add the runner to. Required if unattended
|
||||
--token string Registration token. Required if unattended
|
||||
--name string Name of the runner to configure (default {Environment.MachineName ?? "myrunner"})
|
||||
--work string Relative runner work directory (default {Constants.Path.WorkDirectory})
|
||||
--replace Replace any existing runner with the same name (default false)");
|
||||
--unattended Disable interactive prompts for missing arguments. Defaults will be used for missing options
|
||||
--url string Repository to add the runner to. Required if unattended
|
||||
--token string Registration token. Required if unattended
|
||||
--name string Name of the runner to configure (default {Environment.MachineName ?? "myrunner"})
|
||||
--runnergroup string Name of the runner group to add this runner to (defaults to the default runner group)
|
||||
--labels string Extra labels in addition to the default: 'self-hosted,{Constants.Runner.Platform},{Constants.Runner.PlatformArchitecture}'
|
||||
--work string Relative runner work directory (default {Constants.Path.WorkDirectory})
|
||||
--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`
|
||||
--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
|
||||
_term.WriteLine($@" --runasservice Run the runner as a service");
|
||||
_term.WriteLine($@" --windowslogonaccount string Account to run the service as. Requires runasservice");
|
||||
@@ -475,10 +548,14 @@ Config Options:
|
||||
#endif
|
||||
_term.WriteLine($@"
|
||||
Examples:
|
||||
Check GitHub server network connectivity:
|
||||
.{separator}run.{ext} --check --url <url> --pat <pat>
|
||||
Configure a runner non-interactively:
|
||||
.{separator}config.{ext} --unattended --url <url> --token <token>
|
||||
Configure a runner non-interactively, replacing any existing runner with the same name:
|
||||
.{separator}config.{ext} --unattended --url <url> --token <token> --replace [--name <name>]");
|
||||
.{separator}config.{ext} --unattended --url <url> --token <token> --replace [--name <name>]
|
||||
Configure a runner non-interactively with three extra labels:
|
||||
.{separator}config.{ext} --unattended --url <url> --token <token> --labels L1,L2,L3");
|
||||
#if OS_WINDOWS
|
||||
_term.WriteLine($@" Configure a runner to run as a service:");
|
||||
_term.WriteLine($@" .{separator}config.{ext} --url <url> --token <token> --runasservice");
|
||||
|
||||
@@ -8,9 +8,12 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Cryptography;
|
||||
using GitHub.Services.WebApi;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Text;
|
||||
|
||||
namespace GitHub.Runner.Listener
|
||||
{
|
||||
@@ -61,23 +64,25 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
// Print console line that warn user 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", $"RunnerPlatform: {_targetPackage.Platform}");
|
||||
|
||||
await DownloadLatestRunner(token);
|
||||
var downloadTrace = await DownloadLatestRunner(token);
|
||||
Trace.Info($"Download latest runner and unzip into runner root.");
|
||||
|
||||
// wait till all running job finish
|
||||
await UpdateRunnerUpdateStateAsync("Waiting for current job finish running.");
|
||||
await UpdateRunnerUpdateStateAsync("Waiting for current job finish running.", downloadTrace);
|
||||
|
||||
await jobDispatcher.WaitAsync(token);
|
||||
Trace.Info($"All running job has exited.");
|
||||
|
||||
// We need to keep runner backup around for macOS until we fixed https://github.com/actions/runner/issues/743
|
||||
// delete runner backup
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
DeletePreviousVersionRunnerBackup(token);
|
||||
Trace.Info($"Delete old version runner backup.");
|
||||
|
||||
stopWatch.Stop();
|
||||
// generate update script from template
|
||||
await UpdateRunnerUpdateStateAsync("Generate and execute update script.");
|
||||
await UpdateRunnerUpdateStateAsync("Generate and execute update script.", $"DeleteRunnerBackupTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||
|
||||
string updateScript = GenerateUpdateScript(restartInteractiveRunner);
|
||||
Trace.Info($"Generate update script into: {updateScript}");
|
||||
@@ -94,7 +99,7 @@ namespace GitHub.Runner.Listener
|
||||
invokeScript.Start();
|
||||
Trace.Info($"Update script start running");
|
||||
|
||||
await UpdateRunnerUpdateStateAsync("Runner will exit shortly for update, should back online within 10 seconds.");
|
||||
await UpdateRunnerUpdateStateAsync("Runner will exit shortly for update, should be back online within 10 seconds.", $"RestartInteractiveRunner: {restartInteractiveRunner}");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -110,7 +115,7 @@ namespace GitHub.Runner.Listener
|
||||
// old server won't send target version as part of update message.
|
||||
if (string.IsNullOrEmpty(targetVersion))
|
||||
{
|
||||
var packages = await _runnerServer.GetPackagesAsync(_packageType, _platform, 1, token);
|
||||
var packages = await _runnerServer.GetPackagesAsync(_packageType, _platform, 1, true, token);
|
||||
if (packages == null || packages.Count == 0)
|
||||
{
|
||||
Trace.Info($"There is no package for {_packageType} and {_platform}.");
|
||||
@@ -121,7 +126,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
else
|
||||
{
|
||||
_targetPackage = await _runnerServer.GetPackageAsync(_packageType, _platform, targetVersion, token);
|
||||
_targetPackage = await _runnerServer.GetPackageAsync(_packageType, _platform, targetVersion, true, token);
|
||||
if (_targetPackage == null)
|
||||
{
|
||||
Trace.Info($"There is no package for {_packageType} and {_platform} with version {targetVersion}.");
|
||||
@@ -148,8 +153,10 @@ namespace GitHub.Runner.Listener
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
private async Task DownloadLatestRunner(CancellationToken token)
|
||||
private async Task<string> DownloadLatestRunner(CancellationToken token)
|
||||
{
|
||||
var traceStringBuilder = new StringBuilder();
|
||||
traceStringBuilder.AppendLine($"DownloadUrl: {_targetPackage.DownloadUrl}");
|
||||
string latestRunnerDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), Constants.Path.UpdateDirectory);
|
||||
IOUtil.DeleteDirectory(latestRunnerDirectory, token);
|
||||
Directory.CreateDirectory(latestRunnerDirectory);
|
||||
@@ -158,6 +165,7 @@ namespace GitHub.Runner.Listener
|
||||
string archiveFile = null;
|
||||
bool downloadSucceeded = false;
|
||||
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
// Download the runner, using multiple attempts in order to be resilient against any networking/CDN issues
|
||||
@@ -208,19 +216,35 @@ namespace GitHub.Runner.Listener
|
||||
try
|
||||
{
|
||||
Trace.Info($"Download runner: begin download");
|
||||
long downloadSize = 0;
|
||||
|
||||
//open zip stream in async mode
|
||||
using (HttpClient httpClient = new HttpClient(HostContext.CreateHttpClientHandler()))
|
||||
using (FileStream fs = new FileStream(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
|
||||
using (Stream result = await httpClient.GetStreamAsync(_targetPackage.DownloadUrl))
|
||||
{
|
||||
//81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k).
|
||||
await result.CopyToAsync(fs, 81920, downloadCts.Token);
|
||||
await fs.FlushAsync(downloadCts.Token);
|
||||
if (!string.IsNullOrEmpty(_targetPackage.Token))
|
||||
{
|
||||
Trace.Info($"Adding authorization token ({_targetPackage.Token.Length} chars)");
|
||||
httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _targetPackage.Token);
|
||||
}
|
||||
|
||||
Trace.Info($"Downloading {_targetPackage.DownloadUrl}");
|
||||
|
||||
using (FileStream fs = new FileStream(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
|
||||
using (Stream result = await httpClient.GetStreamAsync(_targetPackage.DownloadUrl))
|
||||
{
|
||||
//81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k).
|
||||
await result.CopyToAsync(fs, 81920, downloadCts.Token);
|
||||
await fs.FlushAsync(downloadCts.Token);
|
||||
downloadSize = fs.Length;
|
||||
}
|
||||
}
|
||||
|
||||
Trace.Info($"Download runner: finished download");
|
||||
downloadSucceeded = true;
|
||||
stopWatch.Stop();
|
||||
traceStringBuilder.AppendLine($"PackageDownloadTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||
traceStringBuilder.AppendLine($"Attempts: {attempt}");
|
||||
traceStringBuilder.AppendLine($"PackageSize: {downloadSize / 1024 / 1024}MB");
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
@@ -245,7 +269,26 @@ namespace GitHub.Runner.Listener
|
||||
throw new TaskCanceledException($"Runner package '{archiveFile}' failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts");
|
||||
}
|
||||
|
||||
stopWatch.Restart();
|
||||
// If we got this far, we know that we've successfully downloaded the runner package
|
||||
// Validate Hash Matches if it is provided
|
||||
using (FileStream stream = File.OpenRead(archiveFile))
|
||||
{
|
||||
if (!String.IsNullOrEmpty(_targetPackage.HashValue))
|
||||
{
|
||||
using (SHA256 sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] srcHashBytes = await sha256.ComputeHashAsync(stream);
|
||||
var hash = PrimitiveExtensions.ConvertToHexString(srcHashBytes);
|
||||
if (hash != _targetPackage.HashValue)
|
||||
{
|
||||
// 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 {_targetPackage.HashValue} for {_targetPackage.Filename}");
|
||||
}
|
||||
Trace.Info($"Validated Runner Hash matches {_targetPackage.Filename} : {_targetPackage.HashValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (archiveFile.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ZipFile.ExtractToDirectory(archiveFile, latestRunnerDirectory);
|
||||
@@ -290,7 +333,9 @@ namespace GitHub.Runner.Listener
|
||||
throw new NotSupportedException($"{archiveFile}");
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
Trace.Info($"Finished getting latest runner package at: {latestRunnerDirectory}.");
|
||||
traceStringBuilder.AppendLine($"PackageExtractTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -310,6 +355,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
stopWatch.Restart();
|
||||
// copy latest runner into runner root folder
|
||||
// copy bin from _work/_update -> bin.version under root
|
||||
string binVersionDir = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), $"{Constants.Path.BinDirectory}.{_targetPackage.Version}");
|
||||
@@ -327,9 +373,19 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Info($"Copy any remaining .sh/.cmd files into runner root.");
|
||||
foreach (FileInfo file in new DirectoryInfo(latestRunnerDirectory).GetFiles() ?? new FileInfo[0])
|
||||
{
|
||||
// Copy and replace the file.
|
||||
file.CopyTo(Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), file.Name), true);
|
||||
string destination = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), file.Name);
|
||||
|
||||
// Removing the file instead of just trying to overwrite it works around permissions issues on linux.
|
||||
// https://github.com/actions/runner/issues/981
|
||||
Trace.Info($"Copy {file.FullName} to {destination}");
|
||||
IOUtil.DeleteFile(destination);
|
||||
file.CopyTo(destination, true);
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
traceStringBuilder.AppendLine($"CopyRunnerToRootTime: {stopWatch.ElapsedMilliseconds}ms");
|
||||
|
||||
return traceStringBuilder.ToString();
|
||||
}
|
||||
|
||||
private void DeletePreviousVersionRunnerBackup(CancellationToken token)
|
||||
@@ -449,13 +505,18 @@ namespace GitHub.Runner.Listener
|
||||
return updateScript;
|
||||
}
|
||||
|
||||
private async Task UpdateRunnerUpdateStateAsync(string currentState)
|
||||
private async Task UpdateRunnerUpdateStateAsync(string currentState, string trace = "")
|
||||
{
|
||||
_terminal.WriteLine(currentState);
|
||||
|
||||
if (!string.IsNullOrEmpty(trace))
|
||||
{
|
||||
Trace.Info(trace);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _runnerServer.UpdateAgentUpdateStateAsync(_poolId, _agentId, currentState);
|
||||
await _runnerServer.UpdateAgentUpdateStateAsync(_poolId, _agentId, currentState, trace);
|
||||
}
|
||||
catch (VssResourceNotFoundException)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<AssetTargetFallback>portable-net45+win8</AssetTargetFallback>
|
||||
<NoWarn>NU1701;NU1603</NoWarn>
|
||||
<Version>$(Version)</Version>
|
||||
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
|
||||
|
||||
@@ -80,7 +80,12 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
// Validate args.
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
executionContext.Output($"Syncing repository: {repoFullName}");
|
||||
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
||||
|
||||
// Repository URL
|
||||
var githubUrl = executionContext.GetGitHubContext("server_url");
|
||||
var githubUri = new Uri(!string.IsNullOrEmpty(githubUrl) ? githubUrl : "https://github.com");
|
||||
var portInfo = githubUri.IsDefaultPort ? string.Empty : $":{githubUri.Port}";
|
||||
Uri repositoryUrl = new Uri($"{githubUri.Scheme}://{githubUri.Host}{portInfo}/{repoFullName}");
|
||||
if (!repositoryUrl.IsAbsoluteUri)
|
||||
{
|
||||
throw new InvalidOperationException("Repository url need to be an absolute uri.");
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<AssetTargetFallback>portable-net45+win8</AssetTargetFallback>
|
||||
<NoWarn>NU1701;NU1603</NoWarn>
|
||||
<Version>$(Version)</Version>
|
||||
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
|
||||
|
||||
19
src/Runner.Sdk/BuildConstants.cs
Normal file
19
src/Runner.Sdk/BuildConstants.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace GitHub.Runner.Sdk
|
||||
{
|
||||
/***
|
||||
* WARNING: This file is automatically regenerated on layout so the runner can provide version/commit info (do not manually edit it).
|
||||
*/
|
||||
public static class BuildConstants
|
||||
{
|
||||
public static class Source
|
||||
{
|
||||
public static readonly string CommitHash = "N/A";
|
||||
}
|
||||
|
||||
public static class RunnerPackage
|
||||
{
|
||||
public static readonly string PackageName = "N/A";
|
||||
public static readonly string Version = "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,7 +318,12 @@ namespace GitHub.Runner.Sdk
|
||||
}
|
||||
}
|
||||
|
||||
using (var registration = cancellationToken.Register(async () => await CancelAndKillProcessTree(killProcessOnCancel)))
|
||||
var cancellationFinished = new TaskCompletionSource<bool>();
|
||||
using (var registration = cancellationToken.Register(async () =>
|
||||
{
|
||||
await CancelAndKillProcessTree(killProcessOnCancel);
|
||||
cancellationFinished.TrySetResult(true);
|
||||
}))
|
||||
{
|
||||
Trace.Info($"Process started with process id {_proc.Id}, waiting for process exit.");
|
||||
while (true)
|
||||
@@ -341,6 +346,13 @@ namespace GitHub.Runner.Sdk
|
||||
// data buffers one last time before returning
|
||||
ProcessOutput();
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Ensure cancellation also finish on the cancellationToken.Register thread.
|
||||
await cancellationFinished.Task;
|
||||
Trace.Info($"Process Cancellation finished.");
|
||||
}
|
||||
|
||||
Trace.Info($"Finished process {_proc.Id} with exit code {_proc.ExitCode}, and elapsed time {_stopWatch.Elapsed}.");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm64;linux-arm;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<AssetTargetFallback>portable-net45+win8</AssetTargetFallback>
|
||||
<NoWarn>NU1701;NU1603</NoWarn>
|
||||
<Version>$(Version)</Version>
|
||||
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user