Compare commits

...

94 Commits

Author SHA1 Message Date
eric sciple
78f21670b4 Fix release workflow to use distinct artifact names 2024-10-03 12:20:00 -07:00
eric sciple
4bf0e1bdb0 v2.320.0 release 2024-10-03 11:53:21 -07:00
eric sciple
149123c232 Prepare v2.320.0 (#3484) 2024-10-03 13:38:35 -05:00
Raj R
e292ec220e Adding Snapshot additional mapping tokens (#3468)
* Adding Snapshot additional mapping tokens

* Lint failure fixes

* Lint failure fixes - 2

* Lint failure fixes - 3

* Fixed a few nits

* Lint fixes

* Removed unncessary white space
2024-10-01 14:04:48 -04:00
Tingluo Huang
3696b7d89f Create launch httpclient using the right handler and setting. (#3476) 2024-09-30 10:57:08 -04:00
Tingluo Huang
6d7446a45e fix missing default user-agent for jitconfig runner. (#3473) 2024-09-25 09:01:53 -04:00
eric sciple
ddf41af767 Cleanup back-compat code for interpreting Run Service status codes (#3456) 2024-09-06 17:04:17 -05:00
Tingluo Huang
0b0cb5520d Add runner or worker to the useragent. (#3457) 2024-09-06 17:16:17 -04:00
Luke Tomlinson
4c0a43f0e4 Handle Error Body in Responses from Broker (#3454) 2024-09-05 17:08:57 -04:00
Tingluo Huang
65764d9ddc Capature actions_type after resolving alpine container. (#3455) 2024-09-05 16:12:29 -04:00
eric sciple
36c66c8083 Fix issues for composite actions (Run Service flow) (#3446) 2024-09-03 17:06:35 -05:00
Tingluo Huang
99b464e102 Trace GitHub RequestId to log. (#3442) 2024-08-27 12:05:26 -04:00
Devin Buhl
e1fa1fcbc3 fix: add jq, git, unzip and curl to default packages installed (#3056)
* fix: add `git` and `curl` to default packages installed

Hi 👋🏼 

These packages are used in a ton of actions on the marketplace. It would be nice if they were installed and ready for use instead of having to install them with `apt-get` on every single Github workflow.

* Update Dockerfile

* Update images/Dockerfile

Co-authored-by: Guillermo Caracuel <633810+gcaracuel@users.noreply.github.com>

* Update images/Dockerfile

Co-authored-by: Tingluo Huang <tingluohuang@github.com>

---------

Co-authored-by: Guillermo Caracuel <633810+gcaracuel@users.noreply.github.com>
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2024-08-20 09:55:30 -04:00
Tingluo Huang
2979fbad94 Add pid to user-agent and session owner. (#3432) 2024-08-16 15:17:13 -04:00
eric sciple
a77fe8a53f .NET 8 compat test adjustments: 1) do not trim SDK, 2) support pattern to match output, 3) modify output truncation length (#3427) 2024-08-13 09:02:26 -05:00
eric sciple
7e84ae0b30 Prepare release 2.319.0 (#3424) 2024-08-08 08:57:44 -05:00
eric sciple
fb6d1adb43 .NET 8 OS compatibility test (#3422)
* Revert "Warn for soon to be deprecated OS versions (#3413)"

This reverts commit ae04147f96.

* Add .NET 8 OS compatibility test

* feedback
2024-08-07 16:53:00 -05:00
Tingluo Huang
7303cb5673 Ignore ssl cert on websocket client. (#3423) 2024-08-06 18:20:54 -04:00
Tingluo Huang
43d67e46db Revert "Bump runner to dotnet 8" (#3412)
* Revert "Upgrade dotnet sdk to v8.0.303 (#3388)"

This reverts commit dbcaa7cf3d.

* Revert "Bump System.Security.Cryptography.Pkcs from 5.0.0 to 8.0.0 in /src (#3347)"

This reverts commit 3dab1f1fb0.

* Revert "Upgrade dotnet sdk to v8.0.302 (#3346)"

This reverts commit 8f1c723ba0.

* Revert "Bump runner to dotnet 8 (#3345)"

This reverts commit 1e74a8137b.
2024-08-05 10:03:18 -05:00
eric sciple
ae04147f96 Warn for soon to be deprecated OS versions (#3413) 2024-08-02 14:37:46 -05:00
eric sciple
12506842c0 Prepare release 2.318.0 (#3404) 2024-07-26 10:03:59 -05:00
Tingluo Huang
2190396357 Update Docker to v27.1.1 (#3401)
* Update Docker to v27.1.1

* Update Dockerfile
2024-07-26 10:36:05 -04:00
Kynan Ware
41bc0da6fe Redirect supported OS doc section to the public docs (#3396)
* redirect supported OS doc to public docs

* Anchor to appropriate OS heading

Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>

---------

Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>
2024-07-22 17:07:09 -04:00
Kynan Ware
2a7f327d93 Update supported distros to match new docs (#3226)
Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>
2024-07-17 10:35:03 -04:00
github-actions[bot]
dbcaa7cf3d Upgrade dotnet sdk to v8.0.303 (#3388)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-07-15 01:06:31 -04:00
Francesco Renzi
8df87a82b0 Rephrase node20 warning (#3376) 2024-07-08 12:41:06 +01:00
Nikola Jokic
70746ff593 Bump hook version to 0.6.1 (#3350) 2024-06-26 14:56:33 +02:00
eric sciple
054fc2e046 Backoff to avoid excessive retries to Run Service in a duration (#3354) 2024-06-24 16:33:22 -05:00
eric sciple
ecb732eaf4 Receive error body from Run Service (#3342) 2024-06-19 16:38:32 +00:00
dependabot[bot]
3dab1f1fb0 Bump System.Security.Cryptography.Pkcs from 5.0.0 to 8.0.0 in /src (#3347)
Bumps [System.Security.Cryptography.Pkcs](https://github.com/dotnet/runtime) from 5.0.0 to 8.0.0.
- [Release notes](https://github.com/dotnet/runtime/releases)
- [Commits](https://github.com/dotnet/runtime/compare/v5.0.0...v8.0.0)

---
updated-dependencies:
- dependency-name: System.Security.Cryptography.Pkcs
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-18 12:12:10 -04:00
github-actions[bot]
8f1c723ba0 Upgrade dotnet sdk to v8.0.302 (#3346)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-06-18 12:03:44 -04:00
Tingluo Huang
1e74a8137b Bump runner to dotnet 8 (#3345)
* Bump runner to dotnet 8

* .
2024-06-18 11:28:53 -04:00
Josh Gross
3f28dd845f Pass runner version as environment variable in workflow (#3318) 2024-06-10 18:13:17 -04:00
Tingluo Huang
edfdbb9661 Make sure we mask secrets when reporting telemetry. (#3315) 2024-06-04 09:57:15 -04:00
Hidetake Iwata
00888c10f9 Bump docker version and docker buildx version (#3277) 2024-05-31 16:22:54 +00:00
Francesco Renzi
84b1bea43e Prepare relese 2.317.0 (#3311) 2024-05-30 13:36:44 +01:00
dependabot[bot]
ce4d7be00f Bump xunit from 2.4.1 to 2.7.1 in /src (#3242)
* Bump xunit from 2.4.1 to 2.7.1 in /src

Bumps [xunit](https://github.com/xunit/xunit) from 2.4.1 to 2.7.1.
- [Commits](https://github.com/xunit/xunit/compare/2.4.1...2.7.1)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Appease xunit warnings after upgrading to v2.7.1

* Appease the whitespace linter

* Appease the whitespace linter

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>
2024-05-21 10:47:43 -04:00
John Wesley Walker III
bd7235ef62 Install gpg-agent during actions/runner container image build (#3294)
`add-apt-repository` depends on `gpg-agent`
2024-05-17 09:55:44 -04:00
John Wesley Walker III
0f15173045 Make it easy to install git on an Action Runner Image (#3273)
(We don't actually install `git`.  We simply get the prerequisites out of the way.)
2024-05-15 18:01:06 +00:00
Patrick Ellis
76dc3a28c0 Upgrade node20: 20.8.1 → 20.13.1 (#3284)
* Upgrade node20: 20.8.1 → 20.13.1

* Call out the release process for `alpine_nodejs` in a comment

* move the comment to the end of the line so it's more obvious which variable it's talking about
2024-05-14 13:50:33 -04:00
Patrick Ellis
c67e7f2813 Delete all the contentHash files (#3285)
Nothing uses them anymore after #3074.
2024-05-13 17:40:23 -04:00
Yang Cao
54052b94fb Also do not give up when uploading steps metadata (#3280) 2024-05-10 11:02:29 -04:00
Luke Tomlinson
f2c05de91c Prep 2.316.1 Release (#3272) 2024-05-02 13:44:48 -04:00
eric sciple
18803bdff6 Preserve dates when deserializing job message from Run Service (#3269)
* Preserve dates when deserializing job message from Run Service

* Preserve dates when deserializing job message from "Actions Run Service"
2024-05-02 10:44:57 -04:00
Francesco Renzi
04b07b6675 Prepare v2.316.0 release (#3252) 2024-04-23 16:46:17 +01:00
eric sciple
dd9fcfc5b2 Replace invalid file name chars in diag log name (#3249) 2024-04-20 11:37:25 -05:00
Tingluo Huang
5107c5efb2 Cleanup enabled feature flags. (#3248) 2024-04-19 15:31:44 -04:00
Yang Cao
1b61d78c07 Relax the condition to stop uploading to Results (#3230) 2024-04-17 13:55:03 +00:00
Tingluo Huang
2e0eb2c11f Cleanup enabled feature flags. (#3246)
* Cleanup FF 'DistributedTask.UseWhich2'.

* more ff.
2024-04-16 16:52:10 -04:00
github-actions[bot]
2d83e1d88f Upgrade dotnet sdk to v6.0.421 (#3244)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-04-14 22:47:26 -04:00
Aiqiao Yan
4a1e38095b backoff if we retried polling for more than 50 times in less than 30m… (#3232)
* backoff if we retried polling for more than 50 times in less than 30minutes

* run dotnet format

* move delay to after no message trace

* run dotnet format
2024-04-09 14:13:07 -04:00
eeSquared
f467e9e125 Add new SessionConflict return code (#3215)
* Add new SessionConflict return code

* formatting

* Change return type of CreateSessionAsync to new enum

* Update entry scripts to handle new exit code

* Move enum
2024-03-27 18:49:58 +00:00
Tingluo Huang
77e0bfbb8a Load '_runnerSettings' in the early point of JobRunner.cs (#3218) 2024-03-23 23:12:39 -04:00
Luke Tomlinson
a52c53955c Prepare v2.315.0 release (#3216) 2024-03-22 11:38:53 -04:00
Luke Tomlinson
8ebf298bcd Always Delete Actions Service Session (#3214)
* Delete Actions Service session always

* update tes
2024-03-21 16:30:34 -04:00
Jacob Wallraff
4b85145661 Handle new non-retryable exception type (#3191)
* Handle new non-retryable exception type

* Update ActionManager.cs
2024-03-21 18:50:45 +00:00
Nikola Jokic
bc8b6e0152 Bump docker version and docker buildx version (#3208) 2024-03-20 11:16:41 -04:00
github-actions[bot]
82e01c6173 Upgrade dotnet sdk to v6.0.420 (#3211)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-17 22:42:44 -04:00
Nikola Jokic
93bc1cd918 Bump hook version to 0.6.0 (#3203) 2024-03-15 11:13:29 -04:00
Tatyana Kostromskaya
692d910868 Add ability to enforce actions to run on node20 (#3192)
Add options to enforce actions execute on node20
2024-03-14 14:12:08 +01:00
Patrick Carnahan
2c8c941622 consume new pipelines service url in handlers (#3185)
* consume pipelines service url if present

updates how the `ACTIONS_RUNTIME_URL` variable is set to utilize a new value, `PipelinesServiceUrl` if present in the endpoint. if this value is not present then the existing system connection endpoint is used to retain backward compatibility.

* consume pipelines url

updates how the `ACTIONS_RUNTIME_URL` variable is set to utilize a new value, `PipelinesServiceUrl` if present in the endpoint. if this value is not present then the existing system connection endpoint is used to retain backward compatibility.
2024-03-05 11:13:16 -05:00
Nikola Jokic
86d6211c75 Remove -f flag in wait when manually trap signal (#3182)
* Remove -f flag in wait when manually trap signal

* Remove extra empty line
2024-03-04 11:32:21 +01:00
Yashwanth Anantharaju
aa90563cae don't crash listener on getting job exceptions (#3177) 2024-02-29 15:39:29 +00:00
Tingluo Huang
4cb3cb2962 Bump runner version to match the latest patch release (#3175) 2024-02-28 20:08:31 +00:00
Ryan Troost
d7777fd632 fix summaries for actions results (#3174)
* fix summaries for actions results

* remove negative
2024-02-27 15:22:26 -05:00
Luke Tomlinson
d8bce88c4f Prepare v2.314.0 release (#3172)
* Prepare v2.314.0 release

* update releaseNoteMd
2024-02-26 13:19:04 -05:00
Tingluo Huang
601d3de3f3 Better step timeout message. (#3166) 2024-02-26 15:37:59 +00:00
Luke Tomlinson
034c51cd0b Refresh Token for BrokerServer (#3167) 2024-02-26 10:05:41 -05:00
Luke Tomlinson
d296014f99 Remove USE_BROKER_FLOW (#3162) 2024-02-21 22:39:26 +00:00
Luke Tomlinson
3449d5fa52 Broker fixes for token refreshes and AccessDeniedException (#3161) 2024-02-21 16:43:01 -05:00
Enes Çakır
6603bfb74c Add a retry logic to docker login operation (#3089)
While there's an existing retry mechanism for the `docker pull` command
[^1], it's missing for `docker login`.

Similar to the `docker pull` scenario, the container registry could
potentially be briefly unavailable or inaccessible, leading to failed
`docker login` attempt and subsequent workflow run failures.

Since it's container based workflow, there is not way to retry on
customer side. The runner should retry itself.

It also aligns with community feedback [^2].

[^1]: 8e0cd36cd8/src/Runner.Worker/ContainerOperationProvider.cs (L201)
[^2]: https://github.com/orgs/community/discussions/73069

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
2024-02-21 17:32:37 +00:00
Yashwanth Anantharaju
b19b9462d8 handle broker run service exception handling (#3163)
* handle run service exception handling

* force fail always

* format

* format
2024-02-21 17:04:13 +00:00
github-actions[bot]
3db5c90cc4 Upgrade dotnet sdk to v6.0.419 (#3158)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-19 00:34:02 -05:00
David Omid
927b26a364 Process snapshot tokens (#3135)
* Added Snapshot TemplateToken to AgentJobRequestMessage

* WIP for processing the snapshot token

* Changed snapshot post job step condition to Success, added comments

* Refactored snapshot post-job step

* Added evaluation of snapshot token to retrieve image name

* Added snapshot to workflow schema

* Fixed linter error

* Migrated snapshot logic to new SnapshotOperationProvider

* Fixed linter error

* Fixed linter errors

* Fixed linter error

* Fixed linter errors

* Updated L0 tests

* Fixed linter errors

* Added new JobExtensionL0 tests for snapshot post-job step

* Added JobExtensionL0 test case for snapshot mappings

* Added SnapshotOperationProviderL0 tests

* Enabled nullable types for SnapshotOperationProvider and its tests

* Added more assertions to SnapshotOperationProviderL0 tests

* Fixed linter errors

* Made sure TestHostContexts are disposed of properlyh in SnapshotOperationProviderL0 tests

* Resolved PR comments

* Fixed formatting

* Removed redundant reference

* Addressed PR comments
2024-02-14 21:34:52 -05:00
Tingluo Huang
72559572f6 Pass RunnerOS during job acquire. (#3140) 2024-02-09 15:19:52 -05:00
Luke Tomlinson
31318d81ba Prepare v2.313.0 Release (#3137)
* update runnerversion

* update releaseNote.md

* update-releasenote
2024-02-07 15:15:32 -05:00
Nikola Jokic
1d47bfa6c7 Bump hook version to 0.5.1 (#3129)
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2024-02-07 19:55:33 +00:00
Luke Tomlinson
651ea42e00 Handle ForceTokenRefresh message (#3133)
* Handle ForceTokenRefresh message

* move to constants

* format
2024-02-07 11:24:40 -05:00
Lukas Hauser
bcc665a7a1 Fix CVE-2024-21626 (#3123)
Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>
2024-02-07 17:06:57 +01:00
Patrick Ellis
cd812f0395 Add sshd to .devcontainer.json (#3079)
This is required in order to ssh into a codespace for the repo: 


> error getting ssh server details: failed to start SSH server:
Please check if an SSH server is installed in the container.
If the docker image being used for the codespace does not have an SSH server,
install it in your Dockerfile or, for codespaces that use Debian-based images,
you can add the following to your devcontainer.json:
> 
> "features": {
>     "ghcr.io/devcontainers/features/sshd:1": {
>         "version": "latest"
>     }
> }
2024-02-06 15:06:07 +00:00
Josh Soref
fa874cf314 Improve error report for invalid action.yml (#3106) 2024-02-06 13:40:53 +00:00
Bethany
bf0e76631b Specify Content-Type for BlockBlob upload (#3119)
* add content-type to block blob upload

* Add content-type for sdk path

* fix spacing

* merge headers and only when file extension is .txt

* add conditions

* tweak conditions and path matching

* pass in headers

* add content-type for appendblob
2024-02-05 18:19:02 +00:00
Yang Cao
1d82031a2c Make sure to drain the upload queue before clean temp directory (#3125) 2024-02-02 20:37:45 +00:00
Victor Sollerhed
d1a619ff09 Upgrade docker from 24.0.8 to 24.0.9 (#3126)
Release notes:
- https://docs.docker.com/engine/release-notes/24.0/#2409
2024-02-02 09:46:12 -05:00
Jonathan Tamsut
11680fc78f Upload the diagnostic logs to the Results Service (#3114)
* upload diagnostic logs to results

* correct casing

* correct typo

* update contract

* update constant and define private method

* fix access

* fix reference to logs url

* update

* use results func

* fix method naming

* rename to match rpc

* change API contract

* fix lint issue

* refactor

* remove unused method

* create new log type
2024-02-01 14:27:06 -05:00
Victor Sollerhed
3e5433ec86 Upgrade docker from 24.0.7 to 24.0.8 (#3124)
Which (among other things) fixes this High `CVE-2024-21626`:
- https://github.com/advisories/GHSA-xr7r-f8xq-vfvv

Release notes:
- https://docs.docker.com/engine/release-notes/24.0/#2408
2024-02-01 11:10:46 -05:00
Tingluo Huang
b647b890c5 Only keep 1 older version runner around after self-upgrade. (#3122) 2024-01-31 10:03:49 -05:00
Luke Tomlinson
894c50073a Implement Broker Redirects for Session and Messages (#3103) 2024-01-30 20:57:49 +00:00
Tingluo Huang
5268d74ade Fix JobDispatcher crash during force cancellation. (#3118) 2024-01-30 15:24:22 -05:00
Tingluo Huang
7414e08fbd Add user-agent to all http clients using RawClientHttpRequestSettings. (#3115) 2024-01-30 13:39:41 -05:00
Tingluo Huang
dcb790f780 Fix release workflow. (#3102) 2024-01-30 13:25:58 -05:00
Tingluo Huang
b7ab810945 Make embedded timeline record has same order as its parent record. (#3113) 2024-01-26 17:26:30 -05:00
Tingluo Huang
7310ba0a08 Revert "Bump container hook version to 0.5.0 in runner image (#3003)" (#3101)
This reverts commit c7d65c42d6.
2024-01-18 11:50:36 -05:00
Diogo Torres
e842959e3e Bump docker and buildx to the latest version (#3100) 2024-01-18 10:42:44 -05:00
121 changed files with 3063 additions and 892 deletions

View File

@@ -4,10 +4,13 @@
"features": { "features": {
"ghcr.io/devcontainers/features/docker-in-docker:1": {}, "ghcr.io/devcontainers/features/docker-in-docker:1": {},
"ghcr.io/devcontainers/features/dotnet": { "ghcr.io/devcontainers/features/dotnet": {
"version": "6.0.418" "version": "6.0.421"
}, },
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "16" "version": "16"
},
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
} }
}, },
"customizations": { "customizations": {

View File

@@ -75,7 +75,7 @@ jobs:
# Upload runner package tar.gz/zip as artifact # Upload runner package tar.gz/zip as artifact
- name: Publish Artifact - name: Publish Artifact
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: runner-package-${{ matrix.runtime }} name: runner-package-${{ matrix.runtime }}
path: | path: |

View File

@@ -25,10 +25,12 @@ jobs:
- name: Compute image version - name: Compute image version
id: image id: image
uses: actions/github-script@v6 uses: actions/github-script@v6
env:
RUNNER_VERSION: ${{ github.event.inputs.runnerVersion }}
with: with:
script: | script: |
const fs = require('fs'); const fs = require('fs');
const inputRunnerVersion = "${{ github.event.inputs.runnerVersion }}" const inputRunnerVersion = process.env.RUNNER_VERSION;
if (inputRunnerVersion) { if (inputRunnerVersion) {
console.log(`Using input runner version ${inputRunnerVersion}`) console.log(`Using input runner version ${inputRunnerVersion}`)
core.setOutput('version', inputRunnerVersion); core.setOutput('version', inputRunnerVersion);

View File

@@ -117,12 +117,11 @@ jobs:
working-directory: _package working-directory: _package
# Upload runner package tar.gz/zip as artifact. # Upload runner package tar.gz/zip as artifact.
# Since each package name is unique, so we don't need to put ${{matrix}} info into artifact name
- name: Publish Artifact - name: Publish Artifact
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: runner-packages name: runner-packages-${{ matrix.runtime }}
path: | path: |
_package _package
@@ -134,10 +133,40 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
# Download runner package tar.gz/zip produced by 'build' job # Download runner package tar.gz/zip produced by 'build' job
- name: Download Artifact - name: Download Artifact (win-x64)
uses: actions/download-artifact@v1 uses: actions/download-artifact@v4
with: with:
name: runner-packages name: runner-packages-win-x64
path: ./
- name: Download Artifact (win-arm64)
uses: actions/download-artifact@v4
with:
name: runner-packages-win-arm64
path: ./
- name: Download Artifact (osx-x64)
uses: actions/download-artifact@v4
with:
name: runner-packages-osx-x64
path: ./
- name: Download Artifact (osx-arm64)
uses: actions/download-artifact@v4
with:
name: runner-packages-osx-arm64
path: ./
- name: Download Artifact (linux-x64)
uses: actions/download-artifact@v4
with:
name: runner-packages-linux-x64
path: ./
- name: Download Artifact (linux-arm)
uses: actions/download-artifact@v4
with:
name: runner-packages-linux-arm
path: ./
- name: Download Artifact (linux-arm64)
uses: actions/download-artifact@v4
with:
name: runner-packages-linux-arm64
path: ./ path: ./
# Create ReleaseNote file # Create ReleaseNote file
@@ -163,7 +192,6 @@ jobs:
core.setOutput('note', releaseNote); core.setOutput('note', releaseNote);
- name: Validate Packages HASH - name: Validate Packages HASH
working-directory: _package
run: | run: |
ls -l ls -l
echo "${{needs.build.outputs.win-x64-sha}} actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip" | shasum -a 256 -c echo "${{needs.build.outputs.win-x64-sha}} actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip" | shasum -a 256 -c
@@ -193,7 +221,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.createRelease.outputs.upload_url }} upload_url: ${{ steps.createRelease.outputs.upload_url }}
asset_path: ${{ github.workspace }}/_package/actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip asset_path: ${{ github.workspace }}/actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip
asset_name: actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip asset_name: actions-runner-win-x64-${{ steps.releaseNote.outputs.version }}.zip
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
@@ -203,7 +231,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.createRelease.outputs.upload_url }} upload_url: ${{ steps.createRelease.outputs.upload_url }}
asset_path: ${{ github.workspace }}/_package/actions-runner-win-arm64-${{ steps.releaseNote.outputs.version }}.zip asset_path: ${{ github.workspace }}/actions-runner-win-arm64-${{ steps.releaseNote.outputs.version }}.zip
asset_name: actions-runner-win-arm64-${{ steps.releaseNote.outputs.version }}.zip asset_name: actions-runner-win-arm64-${{ steps.releaseNote.outputs.version }}.zip
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
@@ -213,7 +241,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.createRelease.outputs.upload_url }} upload_url: ${{ steps.createRelease.outputs.upload_url }}
asset_path: ${{ github.workspace }}/_package/actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.tar.gz asset_path: ${{ github.workspace }}/actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_name: actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.tar.gz asset_name: actions-runner-linux-x64-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
@@ -223,7 +251,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.createRelease.outputs.upload_url }} upload_url: ${{ steps.createRelease.outputs.upload_url }}
asset_path: ${{ github.workspace }}/_package/actions-runner-osx-x64-${{ steps.releaseNote.outputs.version }}.tar.gz asset_path: ${{ github.workspace }}/actions-runner-osx-x64-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_name: actions-runner-osx-x64-${{ steps.releaseNote.outputs.version }}.tar.gz asset_name: actions-runner-osx-x64-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
@@ -233,7 +261,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.createRelease.outputs.upload_url }} upload_url: ${{ steps.createRelease.outputs.upload_url }}
asset_path: ${{ github.workspace }}/_package/actions-runner-osx-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz asset_path: ${{ github.workspace }}/actions-runner-osx-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_name: actions-runner-osx-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz asset_name: actions-runner-osx-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
@@ -243,7 +271,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.createRelease.outputs.upload_url }} upload_url: ${{ steps.createRelease.outputs.upload_url }}
asset_path: ${{ github.workspace }}/_package/actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.tar.gz asset_path: ${{ github.workspace }}/actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_name: actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.tar.gz asset_name: actions-runner-linux-arm-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
@@ -253,7 +281,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.createRelease.outputs.upload_url }} upload_url: ${{ steps.createRelease.outputs.upload_url }}
asset_path: ${{ github.workspace }}/_package/actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz 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_name: actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}.tar.gz
asset_content_type: application/octet-stream asset_content_type: application/octet-stream

View File

@@ -4,16 +4,7 @@
## Supported Distributions and Versions ## Supported Distributions and Versions
x64 Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#linux)."
- Red Hat Enterprise Linux 7+
- CentOS 7+
- Oracle Linux 7+
- Fedora 29+
- Debian 9+
- Ubuntu 16.04+
- Linux Mint 18+
- openSUSE 15+
- SUSE Enterprise Linux (SLES) 12 SP2+
## Install .Net Core 3.x Linux Dependencies ## Install .Net Core 3.x Linux Dependencies

View File

@@ -4,7 +4,6 @@
## Supported Versions ## Supported Versions
- macOS High Sierra (10.13) and later versions Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#macos)."
- x64 and arm64 (Apple Silicon)
## [More .Net Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/macos-prerequisites?tabs=netcore30) ## [More .Net Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/macos-prerequisites?tabs=netcore30)

View File

@@ -2,11 +2,6 @@
## Supported Versions ## Supported Versions
- Windows 7 64-bit Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#windows)."
- Windows 8.1 64-bit
- Windows 10 64-bit
- Windows Server 2012 R2 64-bit
- Windows Server 2016 64-bit
- Windows Server 2019 64-bit
## [More .NET Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore30) ## [More .NET Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore30)

View File

@@ -4,9 +4,9 @@ FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-jammy as build
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
ARG RUNNER_VERSION ARG RUNNER_VERSION
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.5.0 ARG RUNNER_CONTAINER_HOOKS_VERSION=0.6.1
ARG DOCKER_VERSION=24.0.6 ARG DOCKER_VERSION=27.1.1
ARG BUILDX_VERSION=0.11.2 ARG BUILDX_VERSION=0.16.2
RUN apt update -y && apt install curl unzip -y RUN apt update -y && apt install curl unzip -y
@@ -39,12 +39,16 @@ ENV RUNNER_MANUALLY_TRAP_SIG=1
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1 ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
ENV ImageOS=ubuntu22 ENV ImageOS=ubuntu22
RUN apt-get update -y \ # 'gpg-agent' and 'software-properties-common' are needed for the 'add-apt-repository' command that follows
&& apt-get install -y --no-install-recommends \ RUN apt update -y \
sudo \ && apt install -y --no-install-recommends sudo lsb-release gpg-agent software-properties-common curl jq unzip \
lsb-release \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Configure git-core/ppa based on guidance here: https://git-scm.com/download/linux
RUN add-apt-repository ppa:git-core/ppa \
&& apt update -y \
&& apt install -y --no-install-recommends git
RUN adduser --disabled-password --gecos "" --uid 1001 runner \ RUN adduser --disabled-password --gecos "" --uid 1001 runner \
&& groupadd docker --gid 123 \ && groupadd docker --gid 123 \
&& usermod -aG sudo runner \ && usermod -aG sudo runner \

View File

@@ -1,35 +1,28 @@
## What's Changed ## What's Changed
* Fix `buildx` installation by @ajschmidt8 in https://github.com/actions/runner/pull/2952
* Create close-features and close-bugs bot for runner issues by @ruvceskistefan in https://github.com/actions/runner/pull/2909
* Send disableUpdate as query parameter by @luketomlinson in https://github.com/actions/runner/pull/2970
* Handle SelfUpdate Flow when Package is provided in Message by @luketomlinson in https://github.com/actions/runner/pull/2926
* Bump container hook version to 0.5.0 in runner image by @nikola-jokic in https://github.com/actions/runner/pull/3003
* Set `ImageOS` environment variable in runner images by @int128 in https://github.com/actions/runner/pull/2878
* Mark job as failed on worker crash. by @TingluoHuang in https://github.com/actions/runner/pull/3006
* Include whether http proxy configured as part of UserAgent. by @TingluoHuang in https://github.com/actions/runner/pull/3009
* Add codeload to the list of service we check during '--check'. by @TingluoHuang in https://github.com/actions/runner/pull/3011
* close reason update by @ruvceskistefan in https://github.com/actions/runner/pull/3027
* Update envlinux.md by @adjn in https://github.com/actions/runner/pull/3040
* Extend `--check` to check Results-Receiver service. by @TingluoHuang in https://github.com/actions/runner/pull/3078
* Use Azure SDK to upload files to Azure Blob by @yacaovsnc in https://github.com/actions/runner/pull/3033
* Remove code in runner for handling trimmed packages. by @TingluoHuang in https://github.com/actions/runner/pull/3074
* Update dotnet sdk to latest version @6.0.418 by @github-actions in https://github.com/actions/runner/pull/3085
* Patch Curl to no longer use -k by @thboop in https://github.com/actions/runner/pull/3091
## New Contributors - Adding Snapshot additional mapping tokens https://github.com/actions/runner/pull/3468
* @int128 made their first contribution in https://github.com/actions/runner/pull/2878 - Create launch httpclient using the right handler and setting https://github.com/actions/runner/pull/3476
* @adjn made their first contribution in https://github.com/actions/runner/pull/3040 - Fix missing default user-agent for jitconfig runner https://github.com/actions/runner/pull/3473
- Cleanup back-compat code for interpreting Run Service status codes https://github.com/actions/runner/pull/3456
- Add runner or worker to the useragent https://github.com/actions/runner/pull/3457
- Handle Error Body in Responses from Broker https://github.com/actions/runner/pull/3454
- Fix issues for composite actions (Run Service flow) https://github.com/actions/runner/pull/3446
- Trace GitHub RequestId to log https://github.com/actions/runner/pull/3442
- Add `jq`, `git`, `unzip` and `curl` to default packages installed https://github.com/actions/runner/pull/3056
- Add pid to user-agent and session owner https://github.com/actions/runner/pull/3432
**Full Changelog**: https://github.com/actions/runner/compare/v2.311.0...v2.312.0 **Full Changelog**: https://github.com/actions/runner/compare/v2.319.1...v2.320.0
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet. _Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository. To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
See https://docs.github.com/en/enterprise-cloud@latest/actions/hosting-your-own-runners/adding-self-hosted-runners_ See https://docs.github.com/en/enterprise-cloud@latest/actions/hosting-your-own-runners/adding-self-hosted-runners_
## Windows x64 ## Windows x64
We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows. We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows.
The following snipped needs to be run on `powershell`: The following snipped needs to be run on `powershell`:
```powershell ```powershell
# Create a folder under the drive root # Create a folder under the drive root
mkdir \actions-runner ; cd \actions-runner mkdir \actions-runner ; cd \actions-runner
@@ -41,11 +34,13 @@ Add-Type -AssemblyName System.IO.Compression.FileSystem ;
``` ```
## [Pre-release] Windows arm64 ## [Pre-release] Windows arm64
**Warning:** Windows arm64 runners are currently in preview status and use [unofficial versions of nodejs](https://unofficial-builds.nodejs.org/). They are not intended for production workflows. **Warning:** Windows arm64 runners are currently in preview status and use [unofficial versions of nodejs](https://unofficial-builds.nodejs.org/). They are not intended for production workflows.
We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows. We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows.
The following snipped needs to be run on `powershell`: The following snipped needs to be run on `powershell`:
```powershell ```powershell
# Create a folder under the drive root # Create a folder under the drive root
mkdir \actions-runner ; cd \actions-runner mkdir \actions-runner ; cd \actions-runner
@@ -112,6 +107,7 @@ tar xzf ./actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
``` ```
## Using your self hosted runner ## 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) 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 ## SHA-256 Checksums

View File

@@ -1 +1 @@
<Update to ./src/runnerversion when creating release> 2.320.0

View File

@@ -1 +0,0 @@
54d95a44d118dba852395991224a6b9c1abe916858c87138656f80c619e85331

View File

@@ -1 +0,0 @@
68015af17f06a824fa478e62ae7393766ce627fd5599ab916432a14656a19a52

View File

@@ -1 +0,0 @@
a2628119ca419cb54e279103ffae7986cdbd0814d57c73ff0dc74c38be08b9ae

View File

@@ -1 +0,0 @@
de71ca09ead807e1a2ce9df0a5b23eb7690cb71fff51169a77e4c3992be53dda

View File

@@ -1 +0,0 @@
d009e05e6b26d614d65be736a15d1bd151932121c16a9ff1b986deadecc982b9

View File

@@ -1 +0,0 @@
f730db39c2305800b4653795360ba9c10c68f384a46b85d808f1f9f0ed3c42e4

View File

@@ -1 +0,0 @@
a35b5722375490e9473cdcccb5e18b41eba3dbf4344fe31abc9821e21f18ea5a

View File

@@ -1 +0,0 @@
4bf3e1af0d482af1b2eaf9f08250248a8c1aea8ec20a3c5be116d58cdd930009

View File

@@ -1 +0,0 @@
ec1719a8cb4d8687328aa64f4aa7c4e3498a715d8939117874782e3e6e63a14b

View File

@@ -1 +0,0 @@
50538de29f173bb73f708c4ed2c8328a62b8795829b97b2a6cb57197e2305287

View File

@@ -1 +0,0 @@
a0a96cbb7593643b69e669bf14d7b29b7f27800b3a00bb3305aebe041456c701

View File

@@ -1 +0,0 @@
6255b22692779467047ecebd60ad46984866d75cdfe10421d593a7b51d620b09

View File

@@ -1 +0,0 @@
6ff1abd055dc35bfbf06f75c2f08908f660346f66ad1d8f81c910068e9ba029d

View File

@@ -1 +0,0 @@
433a6d748742d12abd20dc2a79b62ac3d9718ae47ef26f8e84dc8c180eea3659

View File

@@ -5,10 +5,11 @@ PRECACHE=$2
NODE_URL=https://nodejs.org/dist NODE_URL=https://nodejs.org/dist
UNOFFICIAL_NODE_URL=https://unofficial-builds.nodejs.org/download/release UNOFFICIAL_NODE_URL=https://unofficial-builds.nodejs.org/download/release
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
NODE16_VERSION="16.20.2" NODE16_VERSION="16.20.2"
NODE20_VERSION="20.8.1" NODE20_VERSION="20.13.1"
# used only for win-arm64, remove node16 unofficial version when official version is available NODE16_UNOFFICIAL_VERSION="16.20.0" # used only for win-arm64, remove node16 unofficial version when official version is available
NODE16_UNOFFICIAL_VERSION="16.20.0"
get_abs_path() { get_abs_path() {
# exploits the fact that pwd will print abs path when no args # exploits the fact that pwd will print abs path when no args

View File

@@ -114,6 +114,11 @@ var runService = function () {
); );
stopping = true; stopping = true;
} }
} else if (code === 5) {
console.log(
"Runner listener exit with Session Conflict error, stop the service, no retry needed."
);
stopping = true;
} else { } else {
var messagePrefix = "Runner listener exit with undefined return code"; var messagePrefix = "Runner listener exit with undefined return code";
unknownFailureRetryCount++; unknownFailureRetryCount++;

View File

@@ -49,5 +49,10 @@ if %ERRORLEVEL% EQU 4 (
exit /b 1 exit /b 1
) )
if %ERRORLEVEL% EQU 5 (
echo "Runner listener exit with Session Conflict error, stop the service, no retry needed."
exit /b 0
)
echo "Exiting after unknown error code: %ERRORLEVEL%" echo "Exiting after unknown error code: %ERRORLEVEL%"
exit /b 0 exit /b 0

View File

@@ -70,6 +70,9 @@ elif [[ $returnCode == 4 ]]; then
"$DIR"/safe_sleep.sh 1 "$DIR"/safe_sleep.sh 1
done done
exit 2 exit 2
elif [[ $returnCode == 5 ]]; then
echo "Runner listener exit with Session Conflict error, stop the service, no retry needed."
exit 0
else else
echo "Exiting with unknown error code: ${returnCode}" echo "Exiting with unknown error code: ${returnCode}"
exit 0 exit 0

View File

@@ -38,7 +38,7 @@ runWithManualTrap() {
cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh cp -f "$DIR"/run-helper.sh.template "$DIR"/run-helper.sh
"$DIR"/run-helper.sh $* & "$DIR"/run-helper.sh $* &
PID=$! PID=$!
wait -f $PID wait $PID
returnCode=$? returnCode=$?
if [[ $returnCode -eq 2 ]]; then if [[ $returnCode -eq 2 ]]; then
echo "Restarting runner..." echo "Restarting runner..."

View File

@@ -20,12 +20,12 @@ namespace GitHub.Runner.Common
{ {
private bool _hasConnection; private bool _hasConnection;
private VssConnection _connection; private VssConnection _connection;
private TaskAgentHttpClient _taskAgentClient; private ActionsRunServerHttpClient _actionsRunServerClient;
public async Task ConnectAsync(Uri serverUrl, VssCredentials credentials) public async Task ConnectAsync(Uri serverUrl, VssCredentials credentials)
{ {
_connection = await EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(100)); _connection = await EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(100));
_taskAgentClient = _connection.GetClient<TaskAgentHttpClient>(); _actionsRunServerClient = _connection.GetClient<ActionsRunServerHttpClient>();
_hasConnection = true; _hasConnection = true;
} }
@@ -42,7 +42,7 @@ namespace GitHub.Runner.Common
CheckConnection(); CheckConnection();
var jobMessage = RetryRequest<AgentJobRequestMessage>(async () => var jobMessage = RetryRequest<AgentJobRequestMessage>(async () =>
{ {
return await _taskAgentClient.GetJobMessageAsync(id, cancellationToken); return await _actionsRunServerClient.GetJobMessageAsync(id, cancellationToken);
}, cancellationToken); }, cancellationToken);
return jobMessage; return jobMessage;

View File

@@ -17,7 +17,14 @@ namespace GitHub.Runner.Common
{ {
Task ConnectAsync(Uri serverUrl, VssCredentials credentials); Task ConnectAsync(Uri serverUrl, VssCredentials credentials);
Task<TaskAgentMessage> GetRunnerMessageAsync(CancellationToken token, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate); Task<TaskAgentSession> CreateSessionAsync(TaskAgentSession session, CancellationToken cancellationToken);
Task DeleteSessionAsync(CancellationToken cancellationToken);
Task<TaskAgentMessage> GetRunnerMessageAsync(Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate, CancellationToken token);
Task UpdateConnectionIfNeeded(Uri serverUri, VssCredentials credentials);
Task ForceRefreshConnection(VssCredentials credentials);
} }
public sealed class BrokerServer : RunnerService, IBrokerServer public sealed class BrokerServer : RunnerService, IBrokerServer
@@ -44,13 +51,53 @@ namespace GitHub.Runner.Common
} }
} }
public Task<TaskAgentMessage> GetRunnerMessageAsync(CancellationToken cancellationToken, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate) public async Task<TaskAgentSession> CreateSessionAsync(TaskAgentSession session, CancellationToken cancellationToken)
{ {
CheckConnection(); CheckConnection();
var jobMessage = RetryRequest<TaskAgentMessage>( var jobMessage = await _brokerHttpClient.CreateSessionAsync(session, cancellationToken);
async () => await _brokerHttpClient.GetRunnerMessageAsync(version, status, os, architecture, disableUpdate, cancellationToken), cancellationToken);
return jobMessage; return jobMessage;
} }
public Task<TaskAgentMessage> GetRunnerMessageAsync(Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate, CancellationToken cancellationToken)
{
CheckConnection();
var brokerSession = RetryRequest<TaskAgentMessage>(
async () => await _brokerHttpClient.GetRunnerMessageAsync(sessionId, version, status, os, architecture, disableUpdate, cancellationToken), cancellationToken, shouldRetry: ShouldRetryException);
return brokerSession;
}
public async Task DeleteSessionAsync(CancellationToken cancellationToken)
{
CheckConnection();
await _brokerHttpClient.DeleteSessionAsync(cancellationToken);
}
public Task UpdateConnectionIfNeeded(Uri serverUri, VssCredentials credentials)
{
if (_brokerUri != serverUri || !_hasConnection)
{
return ConnectAsync(serverUri, credentials);
}
return Task.CompletedTask;
}
public Task ForceRefreshConnection(VssCredentials credentials)
{
return ConnectAsync(_brokerUri, credentials);
}
public bool ShouldRetryException(Exception ex)
{
if (ex is AccessDeniedException ade)
{
return false;
}
return true;
}
} }
} }

View File

@@ -153,6 +153,7 @@ namespace GitHub.Runner.Common
public const int RetryableError = 2; public const int RetryableError = 2;
public const int RunnerUpdating = 3; public const int RunnerUpdating = 3;
public const int RunOnceRunnerUpdating = 4; public const int RunOnceRunnerUpdating = 4;
public const int SessionConflict = 5;
} }
public static class Features public static class Features
@@ -180,6 +181,9 @@ namespace GitHub.Runner.Common
public static readonly string DeprecatedNodeVersion = "node16"; public static readonly string DeprecatedNodeVersion = "node16";
public static readonly string EnforcedNode12DetectedAfterEndOfLife = "The following actions uses node12 which is deprecated and will be forced to run on node16: {0}. For more info: https://github.blog/changelog/2023-06-13-github-actions-all-actions-will-run-on-node16-instead-of-node12-by-default/"; public static readonly string EnforcedNode12DetectedAfterEndOfLife = "The following actions uses node12 which is deprecated and will be forced to run on node16: {0}. For more info: https://github.blog/changelog/2023-06-13-github-actions-all-actions-will-run-on-node16-instead-of-node12-by-default/";
public static readonly string EnforcedNode12DetectedAfterEndOfLifeEnvVariable = "Node16ForceActionsWarnings"; public static readonly string EnforcedNode12DetectedAfterEndOfLifeEnvVariable = "Node16ForceActionsWarnings";
public static readonly string EnforcedNode16DetectedAfterEndOfLife = "The following actions use a deprecated Node.js version and will be forced to run on node20: {0}. For more info: https://github.blog/changelog/2024-03-07-github-actions-all-actions-will-run-on-node20-instead-of-node16-by-default/";
public static readonly string EnforcedNode16DetectedAfterEndOfLifeEnvVariable = "Node20ForceActionsWarnings";
} }
public static class RunnerEvent public static class RunnerEvent
@@ -251,6 +255,7 @@ namespace GitHub.Runner.Common
public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG"; public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG";
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG"; public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
public static readonly string AllowActionsUseUnsecureNodeVersion = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION"; public static readonly string AllowActionsUseUnsecureNodeVersion = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
public static readonly string ManualForceActionsToNode20 = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE20";
} }
public static class Agent public static class Agent
@@ -262,6 +267,7 @@ namespace GitHub.Runner.Common
public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION"; public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT"; public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT";
public static readonly string ActionArchiveCacheDirectory = "ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE"; public static readonly string ActionArchiveCacheDirectory = "ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE";
public static readonly string ManualForceActionsToNode20 = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE20";
} }
public static class System public static class System
@@ -274,6 +280,10 @@ namespace GitHub.Runner.Common
public static readonly string PhaseDisplayName = "system.phaseDisplayName"; public static readonly string PhaseDisplayName = "system.phaseDisplayName";
public static readonly string JobRequestType = "system.jobRequestType"; public static readonly string JobRequestType = "system.jobRequestType";
public static readonly string OrchestrationId = "system.orchestrationId"; public static readonly string OrchestrationId = "system.orchestrationId";
public static readonly string TestDotNet8Compatibility = "system.testDotNet8Compatibility";
public static readonly string DotNet8CompatibilityOutputLength = "system.dotNet8CompatibilityOutputLength";
public static readonly string DotNet8CompatibilityOutputPattern = "system.dotNet8CompatibilityOutputPattern";
public static readonly string DotNet8CompatibilityWarning = "system.dotNet8CompatibilityWarning";
} }
} }

View File

@@ -36,6 +36,7 @@ namespace GitHub.Runner.Common
event EventHandler Unloading; event EventHandler Unloading;
void ShutdownRunner(ShutdownReason reason); void ShutdownRunner(ShutdownReason reason);
void WritePerfCounter(string counter); void WritePerfCounter(string counter);
void LoadDefaultUserAgents();
} }
public enum StartupType public enum StartupType
@@ -67,6 +68,7 @@ namespace GitHub.Runner.Common
private StartupType _startupType; private StartupType _startupType;
private string _perfFile; private string _perfFile;
private RunnerWebProxy _webProxy = new(); private RunnerWebProxy _webProxy = new();
private string _hostType = string.Empty;
public event EventHandler Unloading; public event EventHandler Unloading;
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token; public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
@@ -78,6 +80,7 @@ namespace GitHub.Runner.Common
{ {
// Validate args. // Validate args.
ArgUtil.NotNullOrEmpty(hostType, nameof(hostType)); ArgUtil.NotNullOrEmpty(hostType, nameof(hostType));
_hostType = hostType;
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly); _loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly);
_loadContext.Unloading += LoadContext_Unloading; _loadContext.Unloading += LoadContext_Unloading;
@@ -196,6 +199,16 @@ namespace GitHub.Runner.Common
} }
} }
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
{
_trace.Warning($"Runner is running under insecure mode: HTTPS server certificate validation has been turned off by GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY environment variable.");
}
LoadDefaultUserAgents();
}
public void LoadDefaultUserAgents()
{
if (string.IsNullOrEmpty(WebProxy.HttpProxyAddress) && string.IsNullOrEmpty(WebProxy.HttpsProxyAddress)) if (string.IsNullOrEmpty(WebProxy.HttpProxyAddress) && string.IsNullOrEmpty(WebProxy.HttpsProxyAddress))
{ {
_trace.Info($"No proxy settings were found based on environmental variables (http_proxy/https_proxy/HTTP_PROXY/HTTPS_PROXY)"); _trace.Info($"No proxy settings were found based on environmental variables (http_proxy/https_proxy/HTTP_PROXY/HTTPS_PROXY)");
@@ -205,11 +218,6 @@ namespace GitHub.Runner.Common
_userAgents.Add(new ProductInfoHeaderValue("HttpProxyConfigured", bool.TrueString)); _userAgents.Add(new ProductInfoHeaderValue("HttpProxyConfigured", bool.TrueString));
} }
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
{
_trace.Warning($"Runner is running under insecure mode: HTTPS server certificate validation has been turned off by GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY environment variable.");
}
var credFile = GetConfigFile(WellKnownConfigFile.Credentials); var credFile = GetConfigFile(WellKnownConfigFile.Credentials);
if (File.Exists(credFile)) if (File.Exists(credFile))
{ {
@@ -244,6 +252,11 @@ namespace GitHub.Runner.Common
_trace.Info($"Adding extra user agent '{extraUserAgentHeader}' to all HTTP requests."); _trace.Info($"Adding extra user agent '{extraUserAgentHeader}' to all HTTP requests.");
_userAgents.Add(extraUserAgentHeader); _userAgents.Add(extraUserAgentHeader);
} }
var currentProcess = Process.GetCurrentProcess();
_userAgents.Add(new ProductInfoHeaderValue("Pid", currentProcess.Id.ToString()));
_userAgents.Add(new ProductInfoHeaderValue("CreationTime", Uri.EscapeDataString(DateTime.UtcNow.ToString("O"))));
_userAgents.Add(new ProductInfoHeaderValue($"({_hostType})"));
} }
public string GetDirectory(WellKnownDirectory directory) public string GetDirectory(WellKnownDirectory directory)

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Security;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -179,6 +180,10 @@ namespace GitHub.Runner.Common
userAgentValues.AddRange(UserAgentUtility.GetDefaultRestUserAgent()); userAgentValues.AddRange(UserAgentUtility.GetDefaultRestUserAgent());
userAgentValues.AddRange(HostContext.UserAgents); userAgentValues.AddRange(HostContext.UserAgents);
this._websocketClient.Options.SetRequestHeader("User-Agent", string.Join(" ", userAgentValues.Select(x => x.ToString()))); this._websocketClient.Options.SetRequestHeader("User-Agent", string.Join(" ", userAgentValues.Select(x => x.ToString())));
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
{
this._websocketClient.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true;
}
this._websocketConnectTask = ConnectWebSocketClient(feedStreamUrl, delay); this._websocketConnectTask = ConnectWebSocketClient(feedStreamUrl, delay);
} }

View File

@@ -19,7 +19,7 @@ namespace GitHub.Runner.Common
TaskCompletionSource<int> JobRecordUpdated { get; } TaskCompletionSource<int> JobRecordUpdated { get; }
event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling; event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
Task ShutdownAsync(); Task ShutdownAsync();
void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultsServiceOnly = false, bool enableTelemetry = false); void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultsServiceOnly = false);
void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber = null); void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber = null);
void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource); void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource);
void QueueResultsUpload(Guid timelineRecordId, string name, string path, string type, bool deleteSource, bool finalize, bool firstBlock, long totalLines); void QueueResultsUpload(Guid timelineRecordId, string name, string path, string type, bool deleteSource, bool finalize, bool firstBlock, long totalLines);
@@ -74,6 +74,7 @@ namespace GitHub.Runner.Common
private readonly List<JobTelemetry> _jobTelemetries = new(); private readonly List<JobTelemetry> _jobTelemetries = new();
private bool _queueInProcess = false; private bool _queueInProcess = false;
private bool _resultsServiceOnly = false; private bool _resultsServiceOnly = false;
private int _resultsServiceExceptionsCount = 0;
private Stopwatch _resultsUploadTimer = new(); private Stopwatch _resultsUploadTimer = new();
private Stopwatch _actionsUploadTimer = new(); private Stopwatch _actionsUploadTimer = new();
@@ -104,11 +105,10 @@ namespace GitHub.Runner.Common
_resultsServer = hostContext.GetService<IResultsServer>(); _resultsServer = hostContext.GetService<IResultsServer>();
} }
public void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultsServiceOnly = false, bool enableTelemetry = false) public void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultsServiceOnly = false)
{ {
Trace.Entering(); Trace.Entering();
_resultsServiceOnly = resultsServiceOnly; _resultsServiceOnly = resultsServiceOnly;
_enableTelemetry = enableTelemetry;
var serviceEndPoint = jobRequest.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase)); var serviceEndPoint = jobRequest.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
@@ -139,6 +139,12 @@ namespace GitHub.Runner.Common
_resultsClientInitiated = true; _resultsClientInitiated = true;
} }
// Enable telemetry if we have both results service and actions service
if (_resultsClientInitiated && !_resultsServiceOnly)
{
_enableTelemetry = true;
}
if (_queueInProcess) if (_queueInProcess)
{ {
Trace.Info("No-opt, all queue process tasks are running."); Trace.Info("No-opt, all queue process tasks are running.");
@@ -551,6 +557,10 @@ namespace GitHub.Runner.Common
{ {
await UploadSummaryFile(file); await UploadSummaryFile(file);
} }
if (string.Equals(file.Type, CoreAttachmentType.ResultsDiagnosticLog, StringComparison.OrdinalIgnoreCase))
{
await UploadResultsDiagnosticLogsFile(file);
}
else if (String.Equals(file.Type, CoreAttachmentType.ResultsLog, StringComparison.OrdinalIgnoreCase)) else if (String.Equals(file.Type, CoreAttachmentType.ResultsLog, StringComparison.OrdinalIgnoreCase))
{ {
if (file.RecordId != _jobTimelineRecordId) if (file.RecordId != _jobTimelineRecordId)
@@ -570,9 +580,9 @@ namespace GitHub.Runner.Common
Trace.Info("Catch exception during file upload to results, keep going since the process is best effort."); Trace.Info("Catch exception during file upload to results, keep going since the process is best effort.");
Trace.Error(ex); Trace.Error(ex);
errorCount++; errorCount++;
_resultsServiceExceptionsCount++;
// If we hit any exceptions uploading to Results, let's skip any additional uploads to Results unless Results is serving logs // If we hit any exceptions uploading to Results, let's skip any additional uploads to Results unless Results is serving logs
if (!_resultsServiceOnly) if (!_resultsServiceOnly && _resultsServiceExceptionsCount > 3)
{ {
_resultsClientInitiated = false; _resultsClientInitiated = false;
SendResultsTelemetry(ex); SendResultsTelemetry(ex);
@@ -603,7 +613,7 @@ namespace GitHub.Runner.Common
private void SendResultsTelemetry(Exception ex) private void SendResultsTelemetry(Exception ex)
{ {
var issue = new Issue() { Type = IssueType.Warning, Message = $"Caught exception with results. {ex.Message}" }; var issue = new Issue() { Type = IssueType.Warning, Message = $"Caught exception with results. {HostContext.SecretMasker.MaskSecrets(ex.Message)}" };
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.ResultsUploadFailure; issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.ResultsUploadFailure;
var telemetryRecord = new TimelineRecord() var telemetryRecord = new TimelineRecord()
@@ -699,7 +709,9 @@ namespace GitHub.Runner.Common
{ {
Trace.Info("Catch exception during update steps, skip update Results."); Trace.Info("Catch exception during update steps, skip update Results.");
Trace.Error(e); Trace.Error(e);
if (!_resultsServiceOnly) _resultsServiceExceptionsCount++;
// If we hit any exceptions uploading to Results, let's skip any additional uploads to Results unless Results is serving logs
if (!_resultsServiceOnly && _resultsServiceExceptionsCount > 3)
{ {
_resultsClientInitiated = false; _resultsClientInitiated = false;
SendResultsTelemetry(e); SendResultsTelemetry(e);
@@ -922,6 +934,17 @@ namespace GitHub.Runner.Common
await UploadResultsFile(file, summaryHandler); await UploadResultsFile(file, summaryHandler);
} }
private async Task UploadResultsDiagnosticLogsFile(ResultsUploadFileInfo file)
{
Trace.Info($"Starting to upload diagnostic logs file to results service {file.Name}, {file.Path}");
ResultsFileUploadHandler diagnosticLogsHandler = async (file) =>
{
await _resultsServer.CreateResultsDiagnosticLogsAsync(file.PlanId, file.JobId, file.Path, CancellationToken.None);
};
await UploadResultsFile(file, diagnosticLogsHandler);
}
private async Task UploadResultsStepLogFile(ResultsUploadFileInfo file) private async Task UploadResultsStepLogFile(ResultsUploadFileInfo file)
{ {
Trace.Info($"Starting upload of step log file to results service {file.Name}, {file.Path}"); Trace.Info($"Starting upload of step log file to results service {file.Name}, {file.Path}");

View File

@@ -1,11 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi; using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Sdk;
using GitHub.Services.Common;
using GitHub.Services.Launch.Client; using GitHub.Services.Launch.Client;
using GitHub.Services.WebApi;
namespace GitHub.Runner.Common namespace GitHub.Runner.Common
{ {
@@ -23,8 +24,21 @@ namespace GitHub.Runner.Common
public void InitializeLaunchClient(Uri uri, string token) public void InitializeLaunchClient(Uri uri, string token)
{ {
var httpMessageHandler = HostContext.CreateHttpClientHandler(); // Using default 100 timeout
this._launchClient = new LaunchHttpClient(uri, httpMessageHandler, token, disposeHandler: true); RawClientHttpRequestSettings settings = VssUtil.GetHttpRequestSettings(null);
// Create retry handler
IEnumerable<DelegatingHandler> delegatingHandlers = new List<DelegatingHandler>();
if (settings.MaxRetryRequest > 0)
{
delegatingHandlers = new DelegatingHandler[] { new VssHttpRetryMessageHandler(settings.MaxRetryRequest) };
}
// Setup RawHttpMessageHandler without credentials
var httpMessageHandler = new RawHttpMessageHandler(new NoOpCredentials(null), settings);
var pipeline = HttpClientFactory.CreatePipeline(httpMessageHandler, delegatingHandlers);
this._launchClient = new LaunchHttpClient(uri, pipeline, token, disposeHandler: true);
} }
public Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, public Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList,

View File

@@ -35,6 +35,8 @@ namespace GitHub.Runner.Common
Task UpdateResultsWorkflowStepsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Task UpdateResultsWorkflowStepsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId,
IEnumerable<TimelineRecord> records, CancellationToken cancellationToken); IEnumerable<TimelineRecord> records, CancellationToken cancellationToken);
Task CreateResultsDiagnosticLogsAsync(string planId, string jobId, string file, CancellationToken cancellationToken);
} }
public sealed class ResultServer : RunnerService, IResultsServer public sealed class ResultServer : RunnerService, IResultsServer
@@ -141,6 +143,18 @@ namespace GitHub.Runner.Common
throw new InvalidOperationException("Results client is not initialized."); throw new InvalidOperationException("Results client is not initialized.");
} }
public Task CreateResultsDiagnosticLogsAsync(string planId, string jobId, string file,
CancellationToken cancellationToken)
{
if (_resultsClient != null)
{
return _resultsClient.UploadResultsDiagnosticLogsAsync(planId, jobId, file,
cancellationToken: cancellationToken);
}
throw new InvalidOperationException("Results client is not initialized.");
}
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
CloseWebSocket(WebSocketCloseStatus.NormalClosure, CancellationToken.None); CloseWebSocket(WebSocketCloseStatus.NormalClosure, CancellationToken.None);

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using GitHub.Actions.RunService.WebApi; using GitHub.Actions.RunService.WebApi;
using GitHub.DistributedTask.Pipelines; using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.WebApi; using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Services.Common; using GitHub.Services.Common;
using Sdk.RSWebApi.Contracts; using Sdk.RSWebApi.Contracts;
@@ -60,8 +61,11 @@ namespace GitHub.Runner.Common
{ {
CheckConnection(); CheckConnection();
return RetryRequest<AgentJobRequestMessage>( return RetryRequest<AgentJobRequestMessage>(
async () => await _runServiceHttpClient.GetJobMessageAsync(requestUri, id, cancellationToken), cancellationToken, async () => await _runServiceHttpClient.GetJobMessageAsync(requestUri, id, VarUtil.OS, cancellationToken), cancellationToken,
shouldRetry: ex => ex is not TaskOrchestrationJobAlreadyAcquiredException); shouldRetry: ex =>
ex is not TaskOrchestrationJobNotFoundException && // HTTP status 404
ex is not TaskOrchestrationJobAlreadyAcquiredException && // HTTP status 409
ex is not TaskOrchestrationJobUnprocessableException); // HTTP status 422
} }
public Task CompleteJobAsync( public Task CompleteJobAsync(

View File

@@ -24,7 +24,15 @@ namespace GitHub.Runner.Listener
private TimeSpan _getNextMessageRetryInterval; private TimeSpan _getNextMessageRetryInterval;
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online; private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
private CancellationTokenSource _getMessagesTokenSource; private CancellationTokenSource _getMessagesTokenSource;
private VssCredentials _creds;
private TaskAgentSession _session;
private IBrokerServer _brokerServer; private IBrokerServer _brokerServer;
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
private bool _accessTokenRevoked = false;
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
public override void Initialize(IHostContext hostContext) public override void Initialize(IHostContext hostContext)
{ {
@@ -34,15 +42,141 @@ namespace GitHub.Runner.Listener
_brokerServer = HostContext.GetService<IBrokerServer>(); _brokerServer = HostContext.GetService<IBrokerServer>();
} }
public async Task<Boolean> CreateSessionAsync(CancellationToken token) public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
{ {
await RefreshBrokerConnection(); Trace.Entering();
return await Task.FromResult(true);
// Settings
var configManager = HostContext.GetService<IConfigurationManager>();
_settings = configManager.LoadSettings();
var serverUrl = _settings.ServerUrlV2;
Trace.Info(_settings);
if (string.IsNullOrEmpty(_settings.ServerUrlV2))
{
throw new InvalidOperationException("ServerUrlV2 is not set");
}
// Create connection.
Trace.Info("Loading Credentials");
var credMgr = HostContext.GetService<ICredentialManager>();
_creds = credMgr.LoadCredentials();
var agent = new TaskAgentReference
{
Id = _settings.AgentId,
Name = _settings.AgentName,
Version = BuildConstants.RunnerPackage.Version,
OSDescription = RuntimeInformation.OSDescription,
};
var currentProcess = Process.GetCurrentProcess();
string sessionName = $"{Environment.MachineName ?? "RUNNER"} (PID: {currentProcess.Id})";
var taskAgentSession = new TaskAgentSession(sessionName, agent);
string errorMessage = string.Empty;
bool encounteringError = false;
while (true)
{
token.ThrowIfCancellationRequested();
Trace.Info($"Attempt to create session.");
try
{
Trace.Info("Connecting to the Broker Server...");
await _brokerServer.ConnectAsync(new Uri(serverUrl), _creds);
Trace.Info("VssConnection created");
_term.WriteLine();
_term.WriteSuccessMessage("Connected to GitHub");
_term.WriteLine();
_session = await _brokerServer.CreateSessionAsync(taskAgentSession, token);
Trace.Info($"Session created.");
if (encounteringError)
{
_term.WriteLine($"{DateTime.UtcNow:u}: Runner reconnected.");
_sessionCreationExceptionTracker.Clear();
encounteringError = false;
}
return CreateSessionResult.Success;
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
Trace.Info("Session creation has been cancelled.");
throw;
}
catch (TaskAgentAccessTokenExpiredException)
{
Trace.Info("Runner OAuth token has been revoked. Session creation failed.");
_accessTokenRevoked = true;
throw;
}
catch (Exception ex)
{
Trace.Error("Catch exception during create session.");
Trace.Error(ex);
if (ex is VssOAuthTokenRequestException vssOAuthEx && _creds.Federated is VssOAuthCredential vssOAuthCred)
{
// "invalid_client" means the runner registration has been deleted from the server.
if (string.Equals(vssOAuthEx.Error, "invalid_client", StringComparison.OrdinalIgnoreCase))
{
_term.WriteError("Failed to create a session. The runner registration has been deleted from the server, please re-configure. Runner registrations are automatically deleted for runners that have not connected to the service recently.");
return CreateSessionResult.Failure;
}
// 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))
{
_term.WriteError("Failed to create a session. The runner registration has been deleted from the server, please re-configure. Runner registrations are automatically deleted for runners that have not connected to the service recently.");
return CreateSessionResult.Failure;
}
}
if (!IsSessionCreationExceptionRetriable(ex))
{
_term.WriteError($"Failed to create session. {ex.Message}");
if (ex is TaskAgentSessionConflictException)
{
return CreateSessionResult.SessionConflict;
}
return CreateSessionResult.Failure;
}
if (!encounteringError) //print the message only on the first error
{
_term.WriteError($"{DateTime.UtcNow:u}: Runner connect error: {ex.Message}. Retrying until reconnected.");
encounteringError = true;
}
Trace.Info("Sleeping for {0} seconds before retrying.", _sessionCreationRetryInterval.TotalSeconds);
await HostContext.Delay(_sessionCreationRetryInterval, token);
}
}
} }
public async Task DeleteSessionAsync() public async Task DeleteSessionAsync()
{ {
await Task.CompletedTask; if (_session != null && _session.SessionId != Guid.Empty)
{
if (!_accessTokenRevoked)
{
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{
await _brokerServer.DeleteSessionAsync(ts.Token);
}
}
else
{
Trace.Warning("Runner OAuth token has been revoked. Skip deleting session.");
}
}
} }
public void OnJobStatus(object sender, JobStatusEventArgs e) public void OnJobStatus(object sender, JobStatusEventArgs e)
@@ -73,12 +207,13 @@ namespace GitHub.Runner.Listener
_getMessagesTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); _getMessagesTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
try try
{ {
message = await _brokerServer.GetRunnerMessageAsync(_getMessagesTokenSource.Token, message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
runnerStatus, runnerStatus,
BuildConstants.RunnerPackage.Version, BuildConstants.RunnerPackage.Version,
VarUtil.OS, VarUtil.OS,
VarUtil.OSArchitecture, VarUtil.OSArchitecture,
_settings.DisableUpdate); _settings.DisableUpdate,
_getMessagesTokenSource.Token);
if (message == null) if (message == null)
{ {
@@ -143,7 +278,7 @@ namespace GitHub.Runner.Listener
} }
// re-create VssConnection before next retry // re-create VssConnection before next retry
await RefreshBrokerConnection(); await RefreshBrokerConnectionAsync();
Trace.Info("Sleeping for {0} seconds before retrying.", _getNextMessageRetryInterval.TotalSeconds); Trace.Info("Sleeping for {0} seconds before retrying.", _getNextMessageRetryInterval.TotalSeconds);
await HostContext.Delay(_getNextMessageRetryInterval, token); await HostContext.Delay(_getNextMessageRetryInterval, token);
@@ -173,6 +308,11 @@ namespace GitHub.Runner.Listener
} }
} }
public async Task RefreshListenerTokenAsync(CancellationToken cancellationToken)
{
await RefreshBrokerConnectionAsync();
}
public async Task DeleteMessageAsync(TaskAgentMessage message) public async Task DeleteMessageAsync(TaskAgentMessage message)
{ {
await Task.CompletedTask; await Task.CompletedTask;
@@ -196,12 +336,84 @@ namespace GitHub.Runner.Listener
} }
} }
private async Task RefreshBrokerConnection() private bool IsSessionCreationExceptionRetriable(Exception ex)
{
if (ex is TaskAgentNotFoundException)
{
Trace.Info("The runner no longer exists on the server. Stopping the runner.");
_term.WriteError("The runner no longer exists on the server. Please reconfigure the runner.");
return false;
}
else if (ex is TaskAgentSessionConflictException)
{
Trace.Info("The session for this runner already exists.");
_term.WriteError("A session for this runner already exists.");
if (_sessionCreationExceptionTracker.ContainsKey(nameof(TaskAgentSessionConflictException)))
{
_sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)]++;
if (_sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)] * _sessionCreationRetryInterval.TotalSeconds >= _sessionConflictRetryLimit.TotalSeconds)
{
Trace.Info("The session conflict exception have reached retry limit.");
_term.WriteError($"Stop retry on SessionConflictException after retried for {_sessionConflictRetryLimit.TotalSeconds} seconds.");
return false;
}
}
else
{
_sessionCreationExceptionTracker[nameof(TaskAgentSessionConflictException)] = 1;
}
Trace.Info("The session conflict exception haven't reached retry limit.");
return true;
}
else if (ex is VssOAuthTokenRequestException && ex.Message.Contains("Current server time is"))
{
Trace.Info("Local clock might be skewed.");
_term.WriteError("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.");
if (_sessionCreationExceptionTracker.ContainsKey(nameof(VssOAuthTokenRequestException)))
{
_sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)]++;
if (_sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)] * _sessionCreationRetryInterval.TotalSeconds >= _clockSkewRetryLimit.TotalSeconds)
{
Trace.Info("The OAuth token request exception have reached retry limit.");
_term.WriteError($"Stopped retrying OAuth token request exception after {_clockSkewRetryLimit.TotalSeconds} seconds.");
return false;
}
}
else
{
_sessionCreationExceptionTracker[nameof(VssOAuthTokenRequestException)] = 1;
}
Trace.Info("The OAuth token request exception haven't reached retry limit.");
return true;
}
else if (ex is TaskAgentPoolNotFoundException ||
ex is AccessDeniedException ||
ex is VssUnauthorizedException)
{
Trace.Info($"Non-retriable exception: {ex.Message}");
return false;
}
else if (ex is InvalidOperationException)
{
Trace.Info($"Non-retriable exception: {ex.Message}");
return false;
}
else
{
Trace.Info($"Retriable exception: {ex.Message}");
return true;
}
}
private async Task RefreshBrokerConnectionAsync()
{ {
var configManager = HostContext.GetService<IConfigurationManager>(); var configManager = HostContext.GetService<IConfigurationManager>();
_settings = configManager.LoadSettings(); _settings = configManager.LoadSettings();
if (_settings.ServerUrlV2 == null) if (string.IsNullOrEmpty(_settings.ServerUrlV2))
{ {
throw new InvalidOperationException("ServerUrlV2 is not set"); throw new InvalidOperationException("ServerUrlV2 is not set");
} }

View File

@@ -0,0 +1,44 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Common;
using GitHub.Services.Common;
namespace GitHub.Runner.Listener
{
[ServiceLocator(Default = typeof(ErrorThrottler))]
public interface IErrorThrottler : IRunnerService
{
void Reset();
Task IncrementAndWaitAsync(CancellationToken token);
}
public sealed class ErrorThrottler : RunnerService, IErrorThrottler
{
internal static readonly TimeSpan MinBackoff = TimeSpan.FromSeconds(1);
internal static readonly TimeSpan MaxBackoff = TimeSpan.FromMinutes(1);
internal static readonly TimeSpan BackoffCoefficient = TimeSpan.FromSeconds(1);
private int _count = 0;
public void Reset()
{
_count = 0;
}
public async Task IncrementAndWaitAsync(CancellationToken token)
{
if (++_count <= 1)
{
return;
}
TimeSpan backoff = BackoffTimerHelper.GetExponentialBackoff(
attempt: _count - 2, // 0-based attempt
minBackoff: MinBackoff,
maxBackoff: MaxBackoff,
deltaBackoff: BackoffCoefficient);
Trace.Warning($"Back off {backoff.TotalSeconds} seconds before next attempt. Current consecutive error count: {_count}");
await HostContext.Delay(backoff, token);
}
}
}

View File

@@ -546,13 +546,27 @@ namespace GitHub.Runner.Listener
Trace.Info($"Return code {returnCode} indicate worker encounter an unhandled exception or app crash, attach worker stdout/stderr to JobRequest result."); Trace.Info($"Return code {returnCode} indicate worker encounter an unhandled exception or app crash, attach worker stdout/stderr to JobRequest result.");
var jobServer = await InitializeJobServerAsync(systemConnection); var jobServer = await InitializeJobServerAsync(systemConnection);
await LogWorkerProcessUnhandledException(jobServer, message, detailInfo); var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
switch (jobServer)
{
case IJobServer js:
{
await LogWorkerProcessUnhandledException(js, message, unhandledExceptionIssue);
// 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. // 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)) if (detailInfo.Contains(typeof(System.IO.IOException).ToString(), StringComparison.OrdinalIgnoreCase))
{ {
Trace.Info($"Finish job with result 'Failed' due to IOException."); Trace.Info($"Finish job with result 'Failed' due to IOException.");
await ForceFailJob(jobServer, message, detailInfo); await ForceFailJob(js, message);
}
break;
}
case IRunServer rs:
await ForceFailJob(rs, message, unhandledExceptionIssue);
break;
default:
throw new NotSupportedException($"JobServer type '{jobServer.GetType().Name}' is not supported.");
} }
} }
@@ -629,6 +643,20 @@ namespace GitHub.Runner.Listener
Trace.Info("worker process has been killed."); Trace.Info("worker process has been killed.");
} }
} }
catch (Exception ex)
{
// message send failed, this might indicate worker process is already exited or stuck.
Trace.Info($"Job cancel message sending for job {message.JobId} failed, kill running worker. {ex}");
workerProcessCancelTokenSource.Cancel();
try
{
await workerProcessTask;
}
catch (OperationCanceledException)
{
Trace.Info("worker process has been killed.");
}
}
// wait worker to exit // wait worker to exit
// if worker doesn't exit within timeout, then kill worker. // if worker doesn't exit within timeout, then kill worker.
@@ -1117,9 +1145,7 @@ namespace GitHub.Runner.Listener
} }
// log an error issue to job level timeline record // log an error issue to job level timeline record
private async Task LogWorkerProcessUnhandledException(IRunnerService server, Pipelines.AgentJobRequestMessage message, string detailInfo) private async Task LogWorkerProcessUnhandledException(IJobServer jobServer, Pipelines.AgentJobRequestMessage message, Issue issue)
{
if (server is IJobServer jobServer)
{ {
try try
{ {
@@ -1129,19 +1155,13 @@ namespace GitHub.Runner.Listener
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job"); TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
ArgUtil.NotNull(jobRecord, nameof(jobRecord)); ArgUtil.NotNull(jobRecord, nameof(jobRecord));
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
jobRecord.ErrorCount++; jobRecord.ErrorCount++;
jobRecord.Issues.Add(unhandledExceptionIssue); jobRecord.Issues.Add(issue);
if (message.Variables.TryGetValue("DistributedTask.MarkJobAsFailedOnWorkerCrash", out var markJobAsFailedOnWorkerCrash) &&
StringUtil.ConvertToBoolean(markJobAsFailedOnWorkerCrash?.Value))
{
Trace.Info("Mark the job as failed since the worker crashed"); Trace.Info("Mark the job as failed since the worker crashed");
jobRecord.Result = TaskResult.Failed; jobRecord.Result = TaskResult.Failed;
// mark the job as completed so service will pickup the result // mark the job as completed so service will pickup the result
jobRecord.State = TimelineRecordState.Completed; jobRecord.State = TimelineRecordState.Completed;
}
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None); await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
} }
@@ -1151,17 +1171,9 @@ namespace GitHub.Runner.Listener
Trace.Error(ex); Trace.Error(ex);
} }
} }
else
{
Trace.Info("Job server does not support handling unhandled exception yet, error message: {0}", detailInfo);
return;
}
}
// raise job completed event to fail the job. // raise job completed event to fail the job.
private async Task ForceFailJob(IRunnerService server, Pipelines.AgentJobRequestMessage message, string detailInfo) private async Task ForceFailJob(IJobServer jobServer, Pipelines.AgentJobRequestMessage message)
{
if (server is IJobServer jobServer)
{ {
try try
{ {
@@ -1174,16 +1186,16 @@ namespace GitHub.Runner.Listener
Trace.Error(ex); Trace.Error(ex);
} }
} }
else if (server is IRunServer runServer)
private async Task ForceFailJob(IRunServer runServer, Pipelines.AgentJobRequestMessage message, Issue issue)
{ {
try try
{ {
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo }; var annotation = issue.ToAnnotation();
var unhandledAnnotation = unhandledExceptionIssue.ToAnnotation();
var jobAnnotations = new List<Annotation>(); var jobAnnotations = new List<Annotation>();
if (unhandledAnnotation.HasValue) if (annotation.HasValue)
{ {
jobAnnotations.Add(unhandledAnnotation.Value); jobAnnotations.Add(annotation.Value);
} }
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, environmentUrl: null, CancellationToken.None); await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, environmentUrl: null, CancellationToken.None);
@@ -1194,11 +1206,6 @@ namespace GitHub.Runner.Listener
Trace.Error(ex); Trace.Error(ex);
} }
} }
else
{
throw new NotSupportedException($"Server type {server.GetType().FullName} is not supported.");
}
}
private async Task<IRunnerService> InitializeJobServerAsync(ServiceEndpoint systemConnection) private async Task<IRunnerService> InitializeJobServerAsync(ServiceEndpoint systemConnection)
{ {

View File

@@ -14,16 +14,26 @@ using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Services.Common; using GitHub.Services.Common;
using GitHub.Services.OAuth; using GitHub.Services.OAuth;
using GitHub.Services.WebApi;
namespace GitHub.Runner.Listener namespace GitHub.Runner.Listener
{ {
public enum CreateSessionResult
{
Success,
Failure,
SessionConflict
}
[ServiceLocator(Default = typeof(MessageListener))] [ServiceLocator(Default = typeof(MessageListener))]
public interface IMessageListener : IRunnerService public interface IMessageListener : IRunnerService
{ {
Task<Boolean> CreateSessionAsync(CancellationToken token); Task<CreateSessionResult> CreateSessionAsync(CancellationToken token);
Task DeleteSessionAsync(); Task DeleteSessionAsync();
Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token); Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token);
Task DeleteMessageAsync(TaskAgentMessage message); Task DeleteMessageAsync(TaskAgentMessage message);
Task RefreshListenerTokenAsync(CancellationToken token);
void OnJobStatus(object sender, JobStatusEventArgs e); void OnJobStatus(object sender, JobStatusEventArgs e);
} }
@@ -33,6 +43,7 @@ namespace GitHub.Runner.Listener
private RunnerSettings _settings; private RunnerSettings _settings;
private ITerminal _term; private ITerminal _term;
private IRunnerServer _runnerServer; private IRunnerServer _runnerServer;
private IBrokerServer _brokerServer;
private TaskAgentSession _session; private TaskAgentSession _session;
private TimeSpan _getNextMessageRetryInterval; private TimeSpan _getNextMessageRetryInterval;
private bool _accessTokenRevoked = false; private bool _accessTokenRevoked = false;
@@ -42,6 +53,9 @@ namespace GitHub.Runner.Listener
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new(); private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online; private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
private CancellationTokenSource _getMessagesTokenSource; private CancellationTokenSource _getMessagesTokenSource;
private VssCredentials _creds;
private bool _isBrokerSession = false;
public override void Initialize(IHostContext hostContext) public override void Initialize(IHostContext hostContext)
{ {
@@ -49,9 +63,10 @@ namespace GitHub.Runner.Listener
_term = HostContext.GetService<ITerminal>(); _term = HostContext.GetService<ITerminal>();
_runnerServer = HostContext.GetService<IRunnerServer>(); _runnerServer = HostContext.GetService<IRunnerServer>();
_brokerServer = hostContext.GetService<IBrokerServer>();
} }
public async Task<Boolean> CreateSessionAsync(CancellationToken token) public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
{ {
Trace.Entering(); Trace.Entering();
@@ -64,7 +79,7 @@ namespace GitHub.Runner.Listener
// Create connection. // Create connection.
Trace.Info("Loading Credentials"); Trace.Info("Loading Credentials");
var credMgr = HostContext.GetService<ICredentialManager>(); var credMgr = HostContext.GetService<ICredentialManager>();
VssCredentials creds = credMgr.LoadCredentials(); _creds = credMgr.LoadCredentials();
var agent = new TaskAgentReference var agent = new TaskAgentReference
{ {
@@ -73,7 +88,8 @@ namespace GitHub.Runner.Listener
Version = BuildConstants.RunnerPackage.Version, Version = BuildConstants.RunnerPackage.Version,
OSDescription = RuntimeInformation.OSDescription, OSDescription = RuntimeInformation.OSDescription,
}; };
string sessionName = $"{Environment.MachineName ?? "RUNNER"}"; var currentProcess = Process.GetCurrentProcess();
string sessionName = $"{Environment.MachineName ?? "RUNNER"} (PID: {currentProcess.Id})";
var taskAgentSession = new TaskAgentSession(sessionName, agent); var taskAgentSession = new TaskAgentSession(sessionName, agent);
string errorMessage = string.Empty; string errorMessage = string.Empty;
@@ -86,7 +102,7 @@ namespace GitHub.Runner.Listener
try try
{ {
Trace.Info("Connecting to the Runner Server..."); Trace.Info("Connecting to the Runner Server...");
await _runnerServer.ConnectAsync(new Uri(serverUrl), creds); await _runnerServer.ConnectAsync(new Uri(serverUrl), _creds);
Trace.Info("VssConnection created"); Trace.Info("VssConnection created");
_term.WriteLine(); _term.WriteLine();
@@ -98,6 +114,15 @@ namespace GitHub.Runner.Listener
taskAgentSession, taskAgentSession,
token); token);
if (_session.BrokerMigrationMessage != null)
{
Trace.Info("Runner session is in migration mode: Creating Broker session with BrokerBaseUrl: {0}", _session.BrokerMigrationMessage.BrokerBaseUrl);
await _brokerServer.UpdateConnectionIfNeeded(_session.BrokerMigrationMessage.BrokerBaseUrl, _creds);
_session = await _brokerServer.CreateSessionAsync(taskAgentSession, token);
_isBrokerSession = true;
}
Trace.Info($"Session created."); Trace.Info($"Session created.");
if (encounteringError) if (encounteringError)
{ {
@@ -106,7 +131,7 @@ namespace GitHub.Runner.Listener
encounteringError = false; encounteringError = false;
} }
return true; return CreateSessionResult.Success;
} }
catch (OperationCanceledException) when (token.IsCancellationRequested) catch (OperationCanceledException) when (token.IsCancellationRequested)
{ {
@@ -124,13 +149,13 @@ namespace GitHub.Runner.Listener
Trace.Error("Catch exception during create session."); Trace.Error("Catch exception during create session.");
Trace.Error(ex); Trace.Error(ex);
if (ex is VssOAuthTokenRequestException vssOAuthEx && creds.Federated is VssOAuthCredential vssOAuthCred) if (ex is VssOAuthTokenRequestException vssOAuthEx && _creds.Federated is VssOAuthCredential vssOAuthCred)
{ {
// "invalid_client" means the runner registration has been deleted from the server. // "invalid_client" means the runner registration has been deleted from the server.
if (string.Equals(vssOAuthEx.Error, "invalid_client", StringComparison.OrdinalIgnoreCase)) if (string.Equals(vssOAuthEx.Error, "invalid_client", StringComparison.OrdinalIgnoreCase))
{ {
_term.WriteError("Failed to create a session. The runner registration has been deleted from the server, please re-configure. Runner registrations are automatically deleted for runners that have not connected to the service recently."); _term.WriteError("Failed to create a session. The runner registration has been deleted from the server, please re-configure. Runner registrations are automatically deleted for runners that have not connected to the service recently.");
return false; return CreateSessionResult.Failure;
} }
// Check whether we get 401 because the runner registration already removed by the service. // Check whether we get 401 because the runner registration already removed by the service.
@@ -141,14 +166,18 @@ namespace GitHub.Runner.Listener
if (string.Equals(authError, "invalid_client", StringComparison.OrdinalIgnoreCase)) if (string.Equals(authError, "invalid_client", StringComparison.OrdinalIgnoreCase))
{ {
_term.WriteError("Failed to create a session. The runner registration has been deleted from the server, please re-configure. Runner registrations are automatically deleted for runners that have not connected to the service recently."); _term.WriteError("Failed to create a session. The runner registration has been deleted from the server, please re-configure. Runner registrations are automatically deleted for runners that have not connected to the service recently.");
return false; return CreateSessionResult.Failure;
} }
} }
if (!IsSessionCreationExceptionRetriable(ex)) if (!IsSessionCreationExceptionRetriable(ex))
{ {
_term.WriteError($"Failed to create session. {ex.Message}"); _term.WriteError($"Failed to create session. {ex.Message}");
return false; if (ex is TaskAgentSessionConflictException)
{
return CreateSessionResult.SessionConflict;
}
return CreateSessionResult.Failure;
} }
if (!encounteringError) //print the message only on the first error if (!encounteringError) //print the message only on the first error
@@ -172,6 +201,11 @@ namespace GitHub.Runner.Listener
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30))) using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{ {
await _runnerServer.DeleteAgentSessionAsync(_settings.PoolId, _session.SessionId, ts.Token); await _runnerServer.DeleteAgentSessionAsync(_settings.PoolId, _session.SessionId, ts.Token);
if (_isBrokerSession)
{
await _brokerServer.DeleteSessionAsync(ts.Token);
}
} }
} }
else else
@@ -182,8 +216,6 @@ namespace GitHub.Runner.Listener
} }
public void OnJobStatus(object sender, JobStatusEventArgs e) public void OnJobStatus(object sender, JobStatusEventArgs e)
{
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("USE_BROKER_FLOW")))
{ {
Trace.Info("Received job status event. JobState: {0}", e.Status); Trace.Info("Received job status event. JobState: {0}", e.Status);
runnerStatus = e.Status; runnerStatus = e.Status;
@@ -195,7 +227,7 @@ namespace GitHub.Runner.Listener
{ {
Trace.Info("_getMessagesTokenSource is already disposed."); Trace.Info("_getMessagesTokenSource is already disposed.");
} }
}
} }
public async Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token) public async Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token)
@@ -205,6 +237,7 @@ namespace GitHub.Runner.Listener
ArgUtil.NotNull(_settings, nameof(_settings)); ArgUtil.NotNull(_settings, nameof(_settings));
bool encounteringError = false; bool encounteringError = false;
int continuousError = 0; int continuousError = 0;
int continuousEmptyMessage = 0;
string errorMessage = string.Empty; string errorMessage = string.Empty;
Stopwatch heartbeat = new(); Stopwatch heartbeat = new();
heartbeat.Restart(); heartbeat.Restart();
@@ -228,6 +261,23 @@ namespace GitHub.Runner.Listener
// Decrypt the message body if the session is using encryption // Decrypt the message body if the session is using encryption
message = DecryptMessage(message); message = DecryptMessage(message);
if (message != null && message.MessageType == BrokerMigrationMessage.MessageType)
{
Trace.Info("BrokerMigration message received. Polling Broker for messages...");
var migrationMessage = JsonUtility.FromString<BrokerMigrationMessage>(message.Body);
await _brokerServer.UpdateConnectionIfNeeded(migrationMessage.BrokerBaseUrl, _creds);
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
runnerStatus,
BuildConstants.RunnerPackage.Version,
VarUtil.OS,
VarUtil.OSArchitecture,
_settings.DisableUpdate,
token);
}
if (message != null) if (message != null)
{ {
_lastMessageId = message.MessageId; _lastMessageId = message.MessageId;
@@ -266,7 +316,7 @@ namespace GitHub.Runner.Listener
Trace.Error(ex); Trace.Error(ex);
// don't retry if SkipSessionRecover = true, DT service will delete agent session to stop agent from taking more jobs. // don't retry if SkipSessionRecover = true, DT service will delete agent session to stop agent from taking more jobs.
if (ex is TaskAgentSessionExpiredException && !_settings.SkipSessionRecover && await CreateSessionAsync(token)) if (ex is TaskAgentSessionExpiredException && !_settings.SkipSessionRecover && (await CreateSessionAsync(token) == CreateSessionResult.Success))
{ {
Trace.Info($"{nameof(TaskAgentSessionExpiredException)} received, recovered by recreate session."); Trace.Info($"{nameof(TaskAgentSessionExpiredException)} received, recovered by recreate session.");
} }
@@ -311,16 +361,27 @@ namespace GitHub.Runner.Listener
if (message == null) if (message == null)
{ {
continuousEmptyMessage++;
if (heartbeat.Elapsed > TimeSpan.FromMinutes(30)) if (heartbeat.Elapsed > TimeSpan.FromMinutes(30))
{ {
Trace.Info($"No message retrieved from session '{_session.SessionId}' within last 30 minutes."); Trace.Info($"No message retrieved from session '{_session.SessionId}' within last 30 minutes.");
heartbeat.Restart(); heartbeat.Restart();
continuousEmptyMessage = 0;
} }
else else
{ {
Trace.Verbose($"No message retrieved from session '{_session.SessionId}'."); Trace.Verbose($"No message retrieved from session '{_session.SessionId}'.");
} }
if (continuousEmptyMessage > 50)
{
// retried more than 50 times in less than 30mins and still getting empty message
// something is not right on the service side, backoff for 15-30s before retry
_getNextMessageRetryInterval = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(30), _getNextMessageRetryInterval);
Trace.Info("Sleeping for {0} seconds before retrying.", _getNextMessageRetryInterval.TotalSeconds);
await HostContext.Delay(_getNextMessageRetryInterval, token);
}
continue; continue;
} }
@@ -343,6 +404,12 @@ namespace GitHub.Runner.Listener
} }
} }
public async Task RefreshListenerTokenAsync(CancellationToken cancellationToken)
{
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
await _brokerServer.ForceRefreshConnection(_creds);
}
private TaskAgentMessage DecryptMessage(TaskAgentMessage message) private TaskAgentMessage DecryptMessage(TaskAgentMessage message)
{ {
if (_session.EncryptionKey == null || if (_session.EncryptionKey == null ||

View File

@@ -32,10 +32,25 @@ namespace GitHub.Runner.Listener
private bool _inConfigStage; private bool _inConfigStage;
private ManualResetEvent _completedCommand = new(false); private ManualResetEvent _completedCommand = new(false);
// <summary>
// Helps avoid excessive calls to Run Service when encountering non-retriable errors from /acquirejob.
// Normally we rely on the HTTP clients to back off between retry attempts. However, acquiring a job
// involves calls to both Run Serivce and Broker. And Run Service and Broker communicate with each other
// in an async fashion.
//
// When Run Service encounters a non-retriable error, it sends an async message to Broker. The runner will,
// however, immediately call Broker to get the next message. If the async event from Run Service to Broker
// has not yet been processed, the next message from Broker may be the same job message.
//
// The error throttler helps us back off when encountering successive, non-retriable errors from /acquirejob.
// </summary>
private IErrorThrottler _acquireJobThrottler;
public override void Initialize(IHostContext hostContext) public override void Initialize(IHostContext hostContext)
{ {
base.Initialize(hostContext); base.Initialize(hostContext);
_term = HostContext.GetService<ITerminal>(); _term = HostContext.GetService<ITerminal>();
_acquireJobThrottler = HostContext.CreateService<IErrorThrottler>();
} }
public async Task<int> ExecuteCommand(CommandSettings command) public async Task<int> ExecuteCommand(CommandSettings command)
@@ -222,6 +237,10 @@ namespace GitHub.Runner.Listener
File.SetAttributes(configFile, File.GetAttributes(configFile) | FileAttributes.Hidden); File.SetAttributes(configFile, File.GetAttributes(configFile) | FileAttributes.Hidden);
Trace.Info($"Saved {configContent.Length} bytes to '{configFile}'."); Trace.Info($"Saved {configContent.Length} bytes to '{configFile}'.");
} }
// make sure we have the right user agent data added from the jitconfig
HostContext.LoadDefaultUserAgents();
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -359,7 +378,12 @@ namespace GitHub.Runner.Listener
{ {
Trace.Info(nameof(RunAsync)); Trace.Info(nameof(RunAsync));
_listener = GetMesageListener(settings); _listener = GetMesageListener(settings);
if (!await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken)) CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
if (createSessionResult == CreateSessionResult.SessionConflict)
{
return Constants.Runner.ReturnCode.SessionConflict;
}
else if (createSessionResult == CreateSessionResult.Failure)
{ {
return Constants.Runner.ReturnCode.TerminatedError; return Constants.Runner.ReturnCode.TerminatedError;
} }
@@ -558,13 +582,21 @@ namespace GitHub.Runner.Listener
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), creds); await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), creds);
try try
{ {
jobRequestMessage = jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, _acquireJobThrottler.Reset();
messageQueueLoopTokenSource.Token);
} }
catch (TaskOrchestrationJobAlreadyAcquiredException) catch (Exception ex) when (
ex is TaskOrchestrationJobNotFoundException || // HTTP status 404
ex is TaskOrchestrationJobAlreadyAcquiredException || // HTTP status 409
ex is TaskOrchestrationJobUnprocessableException) // HTTP status 422
{ {
Trace.Info("Job is already acquired, skip this message."); Trace.Info($"Skipping message Job. {ex.Message}");
await _acquireJobThrottler.IncrementAndWaitAsync(messageQueueLoopTokenSource.Token);
continue;
}
catch (Exception ex)
{
Trace.Error($"Caught exception from acquiring job message: {ex}");
continue; continue;
} }
} }
@@ -596,6 +628,11 @@ namespace GitHub.Runner.Listener
Trace.Info($"Service requests the hosted runner to shutdown. Reason: '{HostedRunnerShutdownMessage.Reason}'."); Trace.Info($"Service requests the hosted runner to shutdown. Reason: '{HostedRunnerShutdownMessage.Reason}'.");
return Constants.Runner.ReturnCode.Success; return Constants.Runner.ReturnCode.Success;
} }
else if (string.Equals(message.MessageType, TaskAgentMessageTypes.ForceTokenRefresh))
{
Trace.Info("Received ForceTokenRefreshMessage");
await _listener.RefreshListenerTokenAsync(messageQueueLoopTokenSource.Token);
}
else else
{ {
Trace.Error($"Received message {message.MessageId} with unsupported message type {message.MessageType}."); Trace.Error($"Received message {message.MessageId} with unsupported message type {message.MessageType}.");
@@ -634,6 +671,7 @@ namespace GitHub.Runner.Listener
{ {
try try
{ {
Trace.Info("Deleting Runner Session...");
await _listener.DeleteSessionAsync(); await _listener.DeleteSessionAsync();
} }
catch (Exception ex) when (runOnce) catch (Exception ex) when (runOnce)

View File

@@ -34,6 +34,7 @@ namespace GitHub.Runner.Listener
private IRunnerServer _runnerServer; private IRunnerServer _runnerServer;
private int _poolId; private int _poolId;
private ulong _agentId; private ulong _agentId;
private const int _numberOfOldVersionsToKeep = 1;
private readonly ConcurrentQueue<string> _updateTrace = new(); private readonly ConcurrentQueue<string> _updateTrace = new();
public bool Busy { get; private set; } public bool Busy { get; private set; }
@@ -509,9 +510,9 @@ namespace GitHub.Runner.Listener
// delete old bin.2.99.0 folder, only leave the current version and the latest download version // delete old bin.2.99.0 folder, only leave the current version and the latest download version
var allBinDirs = Directory.GetDirectories(HostContext.GetDirectory(WellKnownDirectory.Root), "bin.*"); var allBinDirs = Directory.GetDirectories(HostContext.GetDirectory(WellKnownDirectory.Root), "bin.*");
if (allBinDirs.Length > 2) if (allBinDirs.Length > _numberOfOldVersionsToKeep)
{ {
// there are more than 2 bin.version folder. // there are more than one bin.version folder.
// delete older bin.version folders. // delete older bin.version folders.
foreach (var oldBinDir in allBinDirs) foreach (var oldBinDir in allBinDirs)
{ {
@@ -538,9 +539,9 @@ namespace GitHub.Runner.Listener
// delete old externals.2.99.0 folder, only leave the current version and the latest download version // delete old externals.2.99.0 folder, only leave the current version and the latest download version
var allExternalsDirs = Directory.GetDirectories(HostContext.GetDirectory(WellKnownDirectory.Root), "externals.*"); var allExternalsDirs = Directory.GetDirectories(HostContext.GetDirectory(WellKnownDirectory.Root), "externals.*");
if (allExternalsDirs.Length > 2) if (allExternalsDirs.Length > _numberOfOldVersionsToKeep)
{ {
// there are more than 2 externals.version folder. // there are more than one externals.version folder.
// delete older externals.version folders. // delete older externals.version folders.
foreach (var oldExternalDir in allExternalsDirs) foreach (var oldExternalDir in allExternalsDirs)
{ {

View File

@@ -459,6 +459,34 @@ namespace GitHub.Runner.Sdk
File.WriteAllText(path, null); File.WriteAllText(path, null);
} }
/// <summary>
/// Replaces invalid file name characters with '_'
/// </summary>
public static string ReplaceInvalidFileNameChars(string fileName)
{
var result = new StringBuilder();
var invalidChars = Path.GetInvalidFileNameChars();
var current = 0; // Current index
while (current < fileName?.Length)
{
var next = fileName.IndexOfAny(invalidChars, current);
if (next >= 0)
{
result.Append(fileName.Substring(current, next - current));
result.Append('_');
current = next + 1;
}
else
{
result.Append(fileName.Substring(current));
break;
}
}
return result.ToString();
}
/// <summary> /// <summary>
/// Recursively enumerates a directory without following directory reparse points. /// Recursively enumerates a directory without following directory reparse points.
/// </summary> /// </summary>

View File

@@ -23,7 +23,13 @@ namespace GitHub.Runner.Sdk
if (VssClientHttpRequestSettings.Default.UserAgent != null && VssClientHttpRequestSettings.Default.UserAgent.Count > 0) if (VssClientHttpRequestSettings.Default.UserAgent != null && VssClientHttpRequestSettings.Default.UserAgent.Count > 0)
{ {
headerValues.AddRange(VssClientHttpRequestSettings.Default.UserAgent); foreach (var headerVal in VssClientHttpRequestSettings.Default.UserAgent)
{
if (!headerValues.Contains(headerVal))
{
headerValues.Add(headerVal);
}
}
} }
VssClientHttpRequestSettings.Default.UserAgent = headerValues; VssClientHttpRequestSettings.Default.UserAgent = headerValues;
@@ -33,6 +39,23 @@ namespace GitHub.Runner.Sdk
{ {
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
} }
var rawHeaderValues = new List<ProductInfoHeaderValue>();
rawHeaderValues.AddRange(additionalUserAgents);
rawHeaderValues.Add(new ProductInfoHeaderValue($"({StringUtil.SanitizeUserAgentHeader(RuntimeInformation.OSDescription)})"));
if (RawClientHttpRequestSettings.Default.UserAgent != null && RawClientHttpRequestSettings.Default.UserAgent.Count > 0)
{
foreach (var headerVal in RawClientHttpRequestSettings.Default.UserAgent)
{
if (!rawHeaderValues.Contains(headerVal))
{
rawHeaderValues.Add(headerVal);
}
}
}
RawClientHttpRequestSettings.Default.UserAgent = rawHeaderValues;
} }
public static VssConnection CreateConnection( public static VssConnection CreateConnection(
@@ -62,11 +85,6 @@ namespace GitHub.Runner.Sdk
settings.SendTimeout = TimeSpan.FromSeconds(Math.Min(Math.Max(httpRequestTimeoutSeconds, 100), 1200)); settings.SendTimeout = TimeSpan.FromSeconds(Math.Min(Math.Max(httpRequestTimeoutSeconds, 100), 1200));
} }
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("USE_BROKER_FLOW")))
{
settings.AllowAutoRedirectForBroker = true;
}
// Remove Invariant from the list of accepted languages. // Remove Invariant from the list of accepted languages.
// //
// The constructor of VssHttpRequestSettings (base class of VssClientHttpRequestSettings) adds the current // The constructor of VssHttpRequestSettings (base class of VssClientHttpRequestSettings) adds the current

View File

@@ -7,129 +7,6 @@ namespace GitHub.Runner.Sdk
public static class WhichUtil public static class WhichUtil
{ {
public static string Which(string command, bool require = false, ITraceWriter trace = null, string prependPath = null) public static string Which(string command, bool require = false, ITraceWriter trace = null, string prependPath = null)
{
ArgUtil.NotNullOrEmpty(command, nameof(command));
trace?.Info($"Which: '{command}'");
if (Path.IsPathFullyQualified(command) && File.Exists(command))
{
trace?.Info($"Fully qualified path: '{command}'");
return command;
}
string path = Environment.GetEnvironmentVariable(PathUtil.PathVariable);
if (string.IsNullOrEmpty(path))
{
trace?.Info("PATH environment variable not defined.");
path = path ?? string.Empty;
}
if (!string.IsNullOrEmpty(prependPath))
{
path = PathUtil.PrependPath(prependPath, path);
}
string[] pathSegments = path.Split(new Char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < pathSegments.Length; i++)
{
pathSegments[i] = Environment.ExpandEnvironmentVariables(pathSegments[i]);
}
foreach (string pathSegment in pathSegments)
{
if (!string.IsNullOrEmpty(pathSegment) && Directory.Exists(pathSegment))
{
string[] matches = null;
#if OS_WINDOWS
string pathExt = Environment.GetEnvironmentVariable("PATHEXT");
if (string.IsNullOrEmpty(pathExt))
{
// XP's system default value for PATHEXT system variable
pathExt = ".com;.exe;.bat;.cmd;.vbs;.vbe;.js;.jse;.wsf;.wsh";
}
string[] pathExtSegments = pathExt.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries);
// if command already has an extension.
if (pathExtSegments.Any(ext => command.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
{
try
{
matches = Directory.GetFiles(pathSegment, command);
}
catch (UnauthorizedAccessException ex)
{
trace?.Info("Ignore UnauthorizedAccess exception during Which.");
trace?.Verbose(ex.ToString());
}
if (matches != null && matches.Length > 0 && IsPathValid(matches.First(), trace))
{
trace?.Info($"Location: '{matches.First()}'");
return matches.First();
}
}
else
{
string searchPattern;
searchPattern = StringUtil.Format($"{command}.*");
try
{
matches = Directory.GetFiles(pathSegment, searchPattern);
}
catch (UnauthorizedAccessException ex)
{
trace?.Info("Ignore UnauthorizedAccess exception during Which.");
trace?.Verbose(ex.ToString());
}
if (matches != null && matches.Length > 0)
{
// add extension.
for (int i = 0; i < pathExtSegments.Length; i++)
{
string fullPath = Path.Combine(pathSegment, $"{command}{pathExtSegments[i]}");
if (matches.Any(p => p.Equals(fullPath, StringComparison.OrdinalIgnoreCase)) && IsPathValid(fullPath, trace))
{
trace?.Info($"Location: '{fullPath}'");
return fullPath;
}
}
}
}
#else
try
{
matches = Directory.GetFiles(pathSegment, command);
}
catch (UnauthorizedAccessException ex)
{
trace?.Info("Ignore UnauthorizedAccess exception during Which.");
trace?.Verbose(ex.ToString());
}
if (matches != null && matches.Length > 0 && IsPathValid(matches.First(), trace))
{
trace?.Info($"Location: '{matches.First()}'");
return matches.First();
}
#endif
}
}
#if OS_WINDOWS
trace?.Info($"{command}: command not found. Make sure '{command}' is installed and its location included in the 'Path' environment variable.");
#else
trace?.Info($"{command}: command not found. Make sure '{command}' is installed and its location included in the 'PATH' environment variable.");
#endif
if (require)
{
throw new FileNotFoundException(
message: $"{command}: command not found",
fileName: command);
}
return null;
}
public static string Which2(string command, bool require = false, ITraceWriter trace = null, string prependPath = null)
{ {
ArgUtil.NotNullOrEmpty(command, nameof(command)); ArgUtil.NotNullOrEmpty(command, nameof(command));
trace?.Info($"Which2: '{command}'"); trace?.Info($"Which2: '{command}'");

View File

@@ -483,10 +483,6 @@ namespace GitHub.Runner.Worker
{ {
// Load stored Ids for later load actions // Load stored Ids for later load actions
compositeAction.Steps[i].Id = _cachedEmbeddedStepIds[action.Id][i]; compositeAction.Steps[i].Id = _cachedEmbeddedStepIds[action.Id][i];
if (string.IsNullOrEmpty(executionContext.Global.Variables.Get("DistributedTask.EnableCompositeActions")) && compositeAction.Steps[i].Reference.Type != Pipelines.ActionSourceType.Script)
{
throw new Exception("`uses:` keyword is not currently supported.");
}
} }
} }
else else
@@ -703,11 +699,12 @@ namespace GitHub.Runner.Worker
catch (Exception ex) when (!executionContext.CancellationToken.IsCancellationRequested) // Do not retry if the run is cancelled. catch (Exception ex) when (!executionContext.CancellationToken.IsCancellationRequested) // Do not retry if the run is cancelled.
{ {
// UnresolvableActionDownloadInfoException is a 422 client error, don't retry // UnresolvableActionDownloadInfoException is a 422 client error, don't retry
// NonRetryableActionDownloadInfoException is an non-retryable exception from Actions
// Some possible cases are: // Some possible cases are:
// * Repo is rate limited // * Repo is rate limited
// * Repo or tag doesn't exist, or isn't public // * Repo or tag doesn't exist, or isn't public
// * Policy validation failed // * Policy validation failed
if (attempt < 3 && !(ex is WebApi.UnresolvableActionDownloadInfoException)) if (attempt < 3 && !(ex is WebApi.UnresolvableActionDownloadInfoException) && !(ex is WebApi.NonRetryableActionDownloadInfoException))
{ {
executionContext.Output($"Failed to resolve action download info. Error: {ex.Message}"); executionContext.Output($"Failed to resolve action download info. Error: {ex.Message}");
executionContext.Debug(ex.ToString()); executionContext.Debug(ex.ToString());
@@ -796,8 +793,6 @@ namespace GitHub.Runner.Worker
try try
{ {
var useActionArchiveCache = false; var useActionArchiveCache = false;
if (executionContext.Global.Variables.GetBoolean("DistributedTask.UseActionArchiveCache") == true)
{
var hasActionArchiveCache = false; var hasActionArchiveCache = false;
var actionArchiveCacheDir = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ActionArchiveCacheDirectory); var actionArchiveCacheDir = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ActionArchiveCacheDirectory);
if (!string.IsNullOrEmpty(actionArchiveCacheDir) && if (!string.IsNullOrEmpty(actionArchiveCacheDir) &&
@@ -831,7 +826,6 @@ namespace GitHub.Runner.Worker
Type = JobTelemetryType.General, Type = JobTelemetryType.General,
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache}" Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache}"
}); });
}
if (!useActionArchiveCache) if (!useActionArchiveCache)
{ {
@@ -877,18 +871,11 @@ namespace GitHub.Runner.Worker
int exitCode = await processInvoker.ExecuteAsync(stagingDirectory, tar, $"-xzf \"{archiveFile}\"", null, executionContext.CancellationToken); int exitCode = await processInvoker.ExecuteAsync(stagingDirectory, tar, $"-xzf \"{archiveFile}\"", null, executionContext.CancellationToken);
if (exitCode != 0) if (exitCode != 0)
{
if (executionContext.Global.Variables.GetBoolean("DistributedTask.DetailUntarFailure") == true)
{ {
var fileInfo = new FileInfo(archiveFile); var fileInfo = new FileInfo(archiveFile);
var sha256hash = await IOUtil.GetFileContentSha256HashAsync(archiveFile); var sha256hash = await IOUtil.GetFileContentSha256HashAsync(archiveFile);
throw new InvalidActionArchiveException($"Can't use 'tar -xzf' extract archive file: {archiveFile} (SHA256 '{sha256hash}', size '{fileInfo.Length}' bytes, tar outputs '{string.Join(' ', tarOutputs)}'). Action being checked out: {downloadInfo.NameWithOwner}@{downloadInfo.Ref}. return code: {exitCode}."); throw new InvalidActionArchiveException($"Can't use 'tar -xzf' extract archive file: {archiveFile} (SHA256 '{sha256hash}', size '{fileInfo.Length}' bytes, tar outputs '{string.Join(' ', tarOutputs)}'). Action being checked out: {downloadInfo.NameWithOwner}@{downloadInfo.Ref}. return code: {exitCode}.");
} }
else
{
throw new InvalidActionArchiveException($"Can't use 'tar -xzf' extract archive file: {archiveFile}. Action being checked out: {downloadInfo.NameWithOwner}@{downloadInfo.Ref}. return code: {exitCode}.");
}
}
} }
#endif #endif
@@ -1031,13 +1018,6 @@ namespace GitHub.Runner.Worker
} }
} }
foreach (var step in compositeAction.Steps)
{
if (string.IsNullOrEmpty(executionContext.Global.Variables.Get("DistributedTask.EnableCompositeActions")) && step.Reference.Type != Pipelines.ActionSourceType.Script)
{
throw new Exception("`uses:` keyword is not currently supported.");
}
}
return setupInfo; return setupInfo;
} }
else else
@@ -1122,6 +1102,7 @@ namespace GitHub.Runner.Worker
int timeoutSeconds = 20 * 60; int timeoutSeconds = 20 * 60;
while (retryCount < 3) while (retryCount < 3)
{ {
string requestId = string.Empty;
using (var actionDownloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds))) using (var actionDownloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
using (var actionDownloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(actionDownloadTimeout.Token, executionContext.CancellationToken)) using (var actionDownloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(actionDownloadTimeout.Token, executionContext.CancellationToken))
{ {
@@ -1137,7 +1118,7 @@ namespace GitHub.Runner.Worker
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
using (var response = await httpClient.GetAsync(downloadUrl)) using (var response = await httpClient.GetAsync(downloadUrl))
{ {
var requestId = UrlUtil.GetGitHubRequestId(response.Headers); requestId = UrlUtil.GetGitHubRequestId(response.Headers);
if (!string.IsNullOrEmpty(requestId)) if (!string.IsNullOrEmpty(requestId))
{ {
Trace.Info($"Request URL: {downloadUrl} X-GitHub-Request-Id: {requestId} Http Status: {response.StatusCode}"); Trace.Info($"Request URL: {downloadUrl} X-GitHub-Request-Id: {requestId} Http Status: {response.StatusCode}");
@@ -1175,7 +1156,7 @@ namespace GitHub.Runner.Worker
catch (OperationCanceledException ex) when (!executionContext.CancellationToken.IsCancellationRequested && retryCount >= 2) catch (OperationCanceledException ex) when (!executionContext.CancellationToken.IsCancellationRequested && retryCount >= 2)
{ {
Trace.Info($"Action download final retry timeout after {timeoutSeconds} seconds."); Trace.Info($"Action download final retry timeout after {timeoutSeconds} seconds.");
throw new TimeoutException($"Action '{downloadUrl}' download has timed out. Error: {ex.Message}"); throw new TimeoutException($"Action '{downloadUrl}' download has timed out. Error: {ex.Message} {requestId}");
} }
catch (ActionNotFoundException) catch (ActionNotFoundException)
{ {
@@ -1190,11 +1171,11 @@ namespace GitHub.Runner.Worker
if (actionDownloadTimeout.Token.IsCancellationRequested) if (actionDownloadTimeout.Token.IsCancellationRequested)
{ {
// action download didn't finish within timeout // action download didn't finish within timeout
executionContext.Warning($"Action '{downloadUrl}' didn't finish download within {timeoutSeconds} seconds."); executionContext.Warning($"Action '{downloadUrl}' didn't finish download within {timeoutSeconds} seconds. {requestId}");
} }
else else
{ {
executionContext.Warning($"Failed to download action '{downloadUrl}'. Error: {ex.Message}"); executionContext.Warning($"Failed to download action '{downloadUrl}'. Error: {ex.Message} {requestId}");
} }
} }
} }

View File

@@ -144,7 +144,7 @@ namespace GitHub.Runner.Worker
executionContext.Error(error.Message); executionContext.Error(error.Message);
} }
throw new ArgumentException($"Fail to load {fileRelativePath}"); throw new ArgumentException($"Failed to load {fileRelativePath}");
} }
if (actionDefinition.Execution == null) if (actionDefinition.Execution == null)

View File

@@ -466,17 +466,39 @@ namespace GitHub.Runner.Worker
{ {
throw new InvalidOperationException($"Failed to create directory to store registry client credentials: {e.Message}"); throw new InvalidOperationException($"Failed to create directory to store registry client credentials: {e.Message}");
} }
var loginExitCode = await _dockerManager.DockerLogin(
// Login docker with retry up to 3 times
int retryCount = 0;
int loginExitCode = 0;
while (retryCount < 3)
{
loginExitCode = await _dockerManager.DockerLogin(
executionContext, executionContext,
configLocation, configLocation,
container.RegistryServer, container.RegistryServer,
container.RegistryAuthUsername, container.RegistryAuthUsername,
container.RegistryAuthPassword); container.RegistryAuthPassword);
if (loginExitCode == 0)
{
break;
}
else
{
retryCount++;
if (retryCount < 3)
{
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
executionContext.Warning($"Docker login for '{container.RegistryServer}' failed with exit code {loginExitCode}, back off {backOff.TotalSeconds} seconds before retry.");
await Task.Delay(backOff);
}
}
}
if (loginExitCode != 0) if (retryCount == 3 && loginExitCode != 0)
{ {
throw new InvalidOperationException($"Docker login for '{container.RegistryServer}' failed with exit code {loginExitCode}"); throw new InvalidOperationException($"Docker login for '{container.RegistryServer}' failed with exit code {loginExitCode}");
} }
return configLocation; return configLocation;
} }

View File

@@ -91,13 +91,13 @@ namespace GitHub.Runner.Worker
string phaseName = executionContext.Global.Variables.System_PhaseDisplayName ?? "UnknownPhaseName"; string phaseName = executionContext.Global.Variables.System_PhaseDisplayName ?? "UnknownPhaseName";
// zip the files // zip the files
string diagnosticsZipFileName = $"{buildName}-{phaseName}.zip"; string diagnosticsZipFileName = $"{buildName}-{IOUtil.ReplaceInvalidFileNameChars(phaseName)}.zip";
string diagnosticsZipFilePath = Path.Combine(supportRootFolder, diagnosticsZipFileName); string diagnosticsZipFilePath = Path.Combine(supportRootFolder, diagnosticsZipFileName);
ZipFile.CreateFromDirectory(supportFilesFolder, diagnosticsZipFilePath); ZipFile.CreateFromDirectory(supportFilesFolder, diagnosticsZipFilePath);
// upload the json metadata file // upload the json metadata file
executionContext.Debug("Uploading diagnostic metadata file."); executionContext.Debug("Uploading diagnostic metadata file.");
string metadataFileName = $"diagnostics-{buildName}-{phaseName}.json"; string metadataFileName = $"diagnostics-{buildName}-{IOUtil.ReplaceInvalidFileNameChars(phaseName)}.json";
string metadataFilePath = Path.Combine(supportFilesFolder, metadataFileName); string metadataFilePath = Path.Combine(supportFilesFolder, metadataFileName);
string phaseResult = GetTaskResultAsString(executionContext.Result); string phaseResult = GetTaskResultAsString(executionContext.Result);
@@ -108,6 +108,8 @@ namespace GitHub.Runner.Worker
parentContext.QueueAttachFile(type: CoreAttachmentType.DiagnosticLog, name: diagnosticsZipFileName, filePath: diagnosticsZipFilePath); parentContext.QueueAttachFile(type: CoreAttachmentType.DiagnosticLog, name: diagnosticsZipFileName, filePath: diagnosticsZipFilePath);
parentContext.QueueDiagnosticLogFile(name: diagnosticsZipFileName, filePath: diagnosticsZipFilePath);
executionContext.Debug("Diagnostic file upload complete."); executionContext.Debug("Diagnostic file upload complete.");
} }

View File

@@ -83,13 +83,14 @@ namespace GitHub.Runner.Worker
// Initialize // Initialize
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token); void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
void CancelToken(); void CancelToken();
IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null, TimeSpan? timeout = null); IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, List<Issue> embeddedIssueCollector = null, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null, TimeSpan? timeout = null);
IExecutionContext CreateEmbeddedChild(string scopeName, string contextName, Guid embeddedId, ActionRunStage stage, Dictionary<string, string> intraActionState = null, string siblingScopeName = null); IExecutionContext CreateEmbeddedChild(string scopeName, string contextName, Guid embeddedId, ActionRunStage stage, Dictionary<string, string> intraActionState = null, string siblingScopeName = null);
// logging // logging
long Write(string tag, string message); long Write(string tag, string message);
void QueueAttachFile(string type, string name, string filePath); void QueueAttachFile(string type, string name, string filePath);
void QueueSummaryFile(string name, string filePath, Guid stepRecordId); void QueueSummaryFile(string name, string filePath, Guid stepRecordId);
void QueueDiagnosticLogFile(string name, string filePath);
// timeline record update methods // timeline record update methods
void Start(string currentOperation = null); void Start(string currentOperation = null);
@@ -134,7 +135,6 @@ namespace GitHub.Runner.Worker
private readonly TimelineRecord _record = new(); private readonly TimelineRecord _record = new();
private readonly Dictionary<Guid, TimelineRecord> _detailRecords = new(); private readonly Dictionary<Guid, TimelineRecord> _detailRecords = new();
private readonly List<Issue> _embeddedIssueCollector;
private readonly object _loggerLock = new(); private readonly object _loggerLock = new();
private readonly object _matchersLock = new(); private readonly object _matchersLock = new();
private readonly ExecutionContext _parentExecutionContext; private readonly ExecutionContext _parentExecutionContext;
@@ -153,6 +153,7 @@ namespace GitHub.Runner.Worker
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
private TaskCompletionSource<int> _forceCompleted = new(); private TaskCompletionSource<int> _forceCompleted = new();
private bool _throttlingReported = false; private bool _throttlingReported = false;
private List<Issue> _embeddedIssueCollector;
// only job level ExecutionContext will track throttling delay. // only job level ExecutionContext will track throttling delay.
private long _totalThrottlingDelayInMilliseconds = 0; private long _totalThrottlingDelayInMilliseconds = 0;
@@ -355,6 +356,7 @@ namespace GitHub.Runner.Worker
int? recordOrder = null, int? recordOrder = null,
IPagingLogger logger = null, IPagingLogger logger = null,
bool isEmbedded = false, bool isEmbedded = false,
List<Issue> embeddedIssueCollector = null,
CancellationTokenSource cancellationTokenSource = null, CancellationTokenSource cancellationTokenSource = null,
Guid embeddedId = default(Guid), Guid embeddedId = default(Guid),
string siblingScopeName = null, string siblingScopeName = null,
@@ -364,6 +366,10 @@ namespace GitHub.Runner.Worker
var child = new ExecutionContext(this, isEmbedded); var child = new ExecutionContext(this, isEmbedded);
child.Initialize(HostContext); child.Initialize(HostContext);
if ((Global.Variables.GetBoolean("RunService.FixEmbeddedIssues") ?? false) && embeddedIssueCollector != null)
{
child._embeddedIssueCollector = embeddedIssueCollector;
}
child.Global = Global; child.Global = Global;
child.ScopeName = scopeName; child.ScopeName = scopeName;
child.ContextName = contextName; child.ContextName = contextName;
@@ -397,11 +403,11 @@ namespace GitHub.Runner.Worker
if (recordOrder != null) if (recordOrder != null)
{ {
child.InitializeTimelineRecord(_mainTimelineId, recordId, _record.Id, ExecutionContextType.Task, displayName, refName, recordOrder); child.InitializeTimelineRecord(_mainTimelineId, recordId, _record.Id, ExecutionContextType.Task, displayName, refName, recordOrder, embedded: isEmbedded);
} }
else else
{ {
child.InitializeTimelineRecord(_mainTimelineId, recordId, _record.Id, ExecutionContextType.Task, displayName, refName, ++_childTimelineRecordOrder); child.InitializeTimelineRecord(_mainTimelineId, recordId, _record.Id, ExecutionContextType.Task, displayName, refName, ++_childTimelineRecordOrder, embedded: isEmbedded);
} }
if (logger != null) if (logger != null)
{ {
@@ -432,7 +438,7 @@ namespace GitHub.Runner.Worker
Dictionary<string, string> intraActionState = null, Dictionary<string, string> intraActionState = null,
string siblingScopeName = null) string siblingScopeName = null)
{ {
return Root.CreateChild(_record.Id, _record.Name, _record.Id.ToString("N"), scopeName, contextName, stage, logger: _logger, isEmbedded: true, cancellationTokenSource: null, intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName, timeout: GetRemainingTimeout()); return Root.CreateChild(_record.Id, _record.Name, _record.Id.ToString("N"), scopeName, contextName, stage, logger: _logger, isEmbedded: true, embeddedIssueCollector: _embeddedIssueCollector, cancellationTokenSource: null, intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName, timeout: GetRemainingTimeout(), recordOrder: _record.Order);
} }
public void Start(string currentOperation = null) public void Start(string currentOperation = null)
@@ -519,7 +525,6 @@ namespace GitHub.Runner.Worker
Global.StepsResult.Add(stepResult); Global.StepsResult.Add(stepResult);
} }
if (Root != this) if (Root != this)
{ {
// only dispose TokenSource for step level ExecutionContext // only dispose TokenSource for step level ExecutionContext
@@ -836,7 +841,6 @@ namespace GitHub.Runner.Worker
// Actions environment // Actions environment
ActionsEnvironment = message.ActionsEnvironment; ActionsEnvironment = message.ActionsEnvironment;
// Service container info // Service container info
Global.ServiceContainers = new List<ContainerInfo>(); Global.ServiceContainers = new List<ContainerInfo>();
@@ -982,6 +986,18 @@ namespace GitHub.Runner.Worker
_jobServerQueue.QueueResultsUpload(stepRecordId, name, filePath, ChecksAttachmentType.StepSummary, deleteSource: false, finalize: true, firstBlock: true, totalLines: 0); _jobServerQueue.QueueResultsUpload(stepRecordId, name, filePath, ChecksAttachmentType.StepSummary, deleteSource: false, finalize: true, firstBlock: true, totalLines: 0);
} }
public void QueueDiagnosticLogFile(string name, string filePath)
{
ArgUtil.NotNullOrEmpty(name, nameof(name));
ArgUtil.NotNullOrEmpty(filePath, nameof(filePath));
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Can't upload diagnostic log file: {filePath}. File does not exist.");
}
_jobServerQueue.QueueResultsUpload(_record.Id, name, filePath, CoreAttachmentType.ResultsDiagnosticLog, deleteSource: false, finalize: true, firstBlock: true, totalLines: 0);
}
// Add OnMatcherChanged // Add OnMatcherChanged
public void Add(OnMatcherChanged handler) public void Add(OnMatcherChanged handler)
{ {
@@ -1160,7 +1176,7 @@ namespace GitHub.Runner.Worker
} }
} }
private void InitializeTimelineRecord(Guid timelineId, Guid timelineRecordId, Guid? parentTimelineRecordId, string recordType, string displayName, string refName, int? order) private void InitializeTimelineRecord(Guid timelineId, Guid timelineRecordId, Guid? parentTimelineRecordId, string recordType, string displayName, string refName, int? order, bool embedded = false)
{ {
_mainTimelineId = timelineId; _mainTimelineId = timelineId;
_record.Id = timelineRecordId; _record.Id = timelineRecordId;
@@ -1186,8 +1202,12 @@ namespace GitHub.Runner.Worker
var configuration = HostContext.GetService<IConfigurationStore>(); var configuration = HostContext.GetService<IConfigurationStore>();
_record.WorkerName = configuration.GetSettings().AgentName; _record.WorkerName = configuration.GetSettings().AgentName;
// We don't want to update the timeline record for embedded steps since they are not really represented in the UI.
if (!embedded)
{
_jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record); _jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record);
} }
}
private void JobServerQueueThrottling_EventReceived(object sender, ThrottlingEventArgs data) private void JobServerQueueThrottling_EventReceived(object sender, ThrottlingEventArgs data)
{ {

View File

@@ -244,7 +244,7 @@ namespace GitHub.Runner.Worker
if (resultsReceiverEndpoint != null) if (resultsReceiverEndpoint != null)
{ {
Trace.Info($"Queueing results file ({filePath}) for attachment upload ({attachmentName})"); Trace.Info($"Queueing results file ({filePath}) for attachment upload ({attachmentName})");
var stepId = context.Id; var stepId = context.IsEmbedded ? context.EmbeddedId : context.Id;
// Attachments must be added to the parent context (job), not the current context (step) // Attachments must be added to the parent context (job), not the current context (step)
context.Root.QueueSummaryFile(attachmentName, scrubbedFilePath, stepId); context.Root.QueueSummaryFile(attachmentName, scrubbedFilePath, stepId);
} }

View File

@@ -223,6 +223,10 @@ namespace GitHub.Runner.Worker.Handlers
{ {
Environment["ACTIONS_CACHE_URL"] = cacheUrl; Environment["ACTIONS_CACHE_URL"] = cacheUrl;
} }
if (systemConnection.Data.TryGetValue("PipelinesServiceUrl", out var pipelinesServiceUrl) && !string.IsNullOrEmpty(pipelinesServiceUrl))
{
Environment["ACTIONS_RUNTIME_URL"] = pipelinesServiceUrl;
}
if (systemConnection.Data.TryGetValue("GenerateIdTokenUrl", out var generateIdTokenUrl) && !string.IsNullOrEmpty(generateIdTokenUrl)) if (systemConnection.Data.TryGetValue("GenerateIdTokenUrl", out var generateIdTokenUrl) && !string.IsNullOrEmpty(generateIdTokenUrl))
{ {
Environment["ACTIONS_ID_TOKEN_REQUEST_URL"] = generateIdTokenUrl; Environment["ACTIONS_ID_TOKEN_REQUEST_URL"] = generateIdTokenUrl;

View File

@@ -84,6 +84,45 @@ namespace GitHub.Runner.Worker.Handlers
} }
nodeData.NodeVersion = "node16"; nodeData.NodeVersion = "node16";
} }
var localForceActionsToNode20 = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Agent.ManualForceActionsToNode20));
executionContext.Global.EnvironmentVariables.TryGetValue(Constants.Variables.Actions.ManualForceActionsToNode20, out var workflowForceActionsToNode20);
var enforceNode20Locally = !string.IsNullOrWhiteSpace(workflowForceActionsToNode20) ? StringUtil.ConvertToBoolean(workflowForceActionsToNode20) : localForceActionsToNode20;
if (string.Equals(nodeData.NodeVersion, "node16")
&& ((executionContext.Global.Variables.GetBoolean("DistributedTask.ForceGithubJavascriptActionsToNode20") ?? false) || enforceNode20Locally))
{
executionContext.Global.EnvironmentVariables.TryGetValue(Constants.Variables.Actions.AllowActionsUseUnsecureNodeVersion, out var workflowOptOut);
var isWorkflowOptOutSet = !string.IsNullOrWhiteSpace(workflowOptOut);
var isLocalOptOut = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Actions.AllowActionsUseUnsecureNodeVersion));
bool isOptOut = isWorkflowOptOutSet ? StringUtil.ConvertToBoolean(workflowOptOut) : isLocalOptOut;
if (!isOptOut)
{
var repoAction = action as Pipelines.RepositoryPathReference;
if (repoAction != null)
{
var warningActions = new HashSet<string>();
if (executionContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode16DetectedAfterEndOfLifeEnvVariable, out var node20ForceWarnings))
{
warningActions = StringUtil.ConvertFromJson<HashSet<string>>(node20ForceWarnings);
}
string repoActionFullName;
if (string.IsNullOrEmpty(repoAction.Name))
{
repoActionFullName = repoAction.Path; // local actions don't have a 'Name'
}
else
{
repoActionFullName = $"{repoAction.Name}/{repoAction.Path ?? string.Empty}".TrimEnd('/') + $"@{repoAction.Ref}";
}
warningActions.Add(repoActionFullName);
executionContext.Global.Variables.Set(Constants.Runner.EnforcedNode16DetectedAfterEndOfLifeEnvVariable, StringUtil.ConvertToJson(warningActions));
}
nodeData.NodeVersion = "node20";
}
}
(handler as INodeScriptActionHandler).Data = nodeData; (handler as INodeScriptActionHandler).Data = nodeData;
} }
else if (data.ExecutionType == ActionExecutionType.Script) else if (data.ExecutionType == ActionExecutionType.Script)

View File

@@ -58,6 +58,10 @@ namespace GitHub.Runner.Worker.Handlers
{ {
Environment["ACTIONS_CACHE_URL"] = cacheUrl; Environment["ACTIONS_CACHE_URL"] = cacheUrl;
} }
if (systemConnection.Data.TryGetValue("PipelinesServiceUrl", out var pipelinesServiceUrl) && !string.IsNullOrEmpty(pipelinesServiceUrl))
{
Environment["ACTIONS_RUNTIME_URL"] = pipelinesServiceUrl;
}
if (systemConnection.Data.TryGetValue("GenerateIdTokenUrl", out var generateIdTokenUrl) && !string.IsNullOrEmpty(generateIdTokenUrl)) if (systemConnection.Data.TryGetValue("GenerateIdTokenUrl", out var generateIdTokenUrl) && !string.IsNullOrEmpty(generateIdTokenUrl))
{ {
Environment["ACTIONS_ID_TOKEN_REQUEST_URL"] = generateIdTokenUrl; Environment["ACTIONS_ID_TOKEN_REQUEST_URL"] = generateIdTokenUrl;
@@ -89,7 +93,6 @@ namespace GitHub.Runner.Worker.Handlers
ExecutionContext.StepTelemetry.HasPreStep = Data.HasPre; ExecutionContext.StepTelemetry.HasPreStep = Data.HasPre;
ExecutionContext.StepTelemetry.HasPostStep = Data.HasPost; ExecutionContext.StepTelemetry.HasPostStep = Data.HasPost;
} }
ExecutionContext.StepTelemetry.Type = Data.NodeVersion;
ArgUtil.NotNullOrEmpty(target, nameof(target)); ArgUtil.NotNullOrEmpty(target, nameof(target));
target = Path.Combine(ActionDirectory, target); target = Path.Combine(ActionDirectory, target);
@@ -114,7 +117,13 @@ namespace GitHub.Runner.Worker.Handlers
{ {
Data.NodeVersion = "node16"; Data.NodeVersion = "node16";
} }
if (forcedNodeVersion == "node20" && Data.NodeVersion != "node20")
{
Data.NodeVersion = "node20";
}
var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext, Data.NodeVersion); var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext, Data.NodeVersion);
ExecutionContext.StepTelemetry.Type = nodeRuntimeVersion;
string file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}"); string file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}");
// Format the arguments passed to node. // Format the arguments passed to node.

View File

@@ -82,42 +82,21 @@ namespace GitHub.Runner.Worker.Handlers
#if OS_WINDOWS #if OS_WINDOWS
shellCommand = "pwsh"; shellCommand = "pwsh";
if (validateShellOnHost) if (validateShellOnHost)
{
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
shellCommandPath = WhichUtil.Which2(shellCommand, require: false, Trace, prependPath);
}
else
{ {
shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath); shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
}
if (string.IsNullOrEmpty(shellCommandPath)) if (string.IsNullOrEmpty(shellCommandPath))
{ {
shellCommand = "powershell"; shellCommand = "powershell";
Trace.Info($"Defaulting to {shellCommand}"); Trace.Info($"Defaulting to {shellCommand}");
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
shellCommandPath = WhichUtil.Which2(shellCommand, require: true, Trace, prependPath);
}
else
{
shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath); shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
} }
} }
}
#else #else
shellCommand = "sh"; shellCommand = "sh";
if (validateShellOnHost) if (validateShellOnHost)
{
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
shellCommandPath = WhichUtil.Which2("bash", false, Trace, prependPath) ?? WhichUtil.Which2("sh", true, Trace, prependPath);
}
else
{ {
shellCommandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath); shellCommandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
} }
}
#endif #endif
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand); argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
} }
@@ -126,16 +105,9 @@ namespace GitHub.Runner.Worker.Handlers
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell); var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand; shellCommand = parsed.shellCommand;
if (validateShellOnHost) if (validateShellOnHost)
{
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
shellCommandPath = WhichUtil.Which2(parsed.shellCommand, true, Trace, prependPath);
}
else
{ {
shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace, prependPath); shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace, prependPath);
} }
}
argFormat = $"{parsed.shellArgs}".TrimStart(); argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat)) if (string.IsNullOrEmpty(argFormat))
@@ -216,38 +188,17 @@ namespace GitHub.Runner.Worker.Handlers
{ {
#if OS_WINDOWS #if OS_WINDOWS
shellCommand = "pwsh"; shellCommand = "pwsh";
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2(shellCommand, require: false, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath); commandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
}
if (string.IsNullOrEmpty(commandPath)) if (string.IsNullOrEmpty(commandPath))
{ {
shellCommand = "powershell"; shellCommand = "powershell";
Trace.Info($"Defaulting to {shellCommand}"); Trace.Info($"Defaulting to {shellCommand}");
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2(shellCommand, require: true, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath); commandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
} }
}
ArgUtil.NotNullOrEmpty(commandPath, "Default Shell"); ArgUtil.NotNullOrEmpty(commandPath, "Default Shell");
#else #else
shellCommand = "sh"; shellCommand = "sh";
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2("bash", false, Trace, prependPath) ?? WhichUtil.Which2("sh", true, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath); commandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
}
#endif #endif
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand); argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
} }
@@ -258,14 +209,7 @@ namespace GitHub.Runner.Worker.Handlers
if (!IsActionStep && systemShells.Contains(shell)) if (!IsActionStep && systemShells.Contains(shell))
{ {
shellCommand = shell; shellCommand = shell;
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2(shell, !isContainerStepHost, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which(shell, !isContainerStepHost, Trace, prependPath); commandPath = WhichUtil.Which(shell, !isContainerStepHost, Trace, prependPath);
}
if (shell == "bash") if (shell == "bash")
{ {
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat("sh"); argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat("sh");
@@ -280,14 +224,7 @@ namespace GitHub.Runner.Worker.Handlers
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell); var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand; shellCommand = parsed.shellCommand;
// For non-ContainerStepHost, the command must be located on the host by Which // For non-ContainerStepHost, the command must be located on the host by Which
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath); commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
}
argFormat = $"{parsed.shellArgs}".TrimStart(); argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat)) if (string.IsNullOrEmpty(argFormat))
{ {

View File

@@ -127,6 +127,10 @@ namespace GitHub.Runner.Worker
} }
} }
// Check OS warning
var osWarningChecker = HostContext.GetService<IOSWarningChecker>();
await osWarningChecker.CheckOSAsync(context);
try try
{ {
var tokenPermissions = jobContext.Global.Variables.Get("system.github.token.permissions") ?? ""; var tokenPermissions = jobContext.Global.Variables.Get("system.github.token.permissions") ?? "";
@@ -392,6 +396,18 @@ namespace GitHub.Runner.Worker
} }
} }
// Register custom image creation post-job step if the "snapshot" token is present in the message.
var snapshotRequest = templateEvaluator.EvaluateJobSnapshotRequest(message.Snapshot, jobContext.ExpressionValues, jobContext.ExpressionFunctions);
if (snapshotRequest != null)
{
var snapshotOperationProvider = HostContext.GetService<ISnapshotOperationProvider>();
jobContext.RegisterPostJobStep(new JobExtensionRunner(
runAsync: (executionContext, _) => snapshotOperationProvider.CreateSnapshotRequestAsync(executionContext, snapshotRequest),
condition: snapshotRequest.Condition,
displayName: $"Create custom image",
data: null));
}
// Register Job Completed hook if the variable is set // Register Job Completed hook if the variable is set
var completedHookPath = Environment.GetEnvironmentVariable("ACTIONS_RUNNER_HOOK_JOB_COMPLETED"); var completedHookPath = Environment.GetEnvironmentVariable("ACTIONS_RUNNER_HOOK_JOB_COMPLETED");
if (!string.IsNullOrEmpty(completedHookPath)) if (!string.IsNullOrEmpty(completedHookPath))

View File

@@ -42,6 +42,7 @@ namespace GitHub.Runner.Worker
Trace.Info("Job ID {0}", message.JobId); Trace.Info("Job ID {0}", message.JobId);
DateTime jobStartTimeUtc = DateTime.UtcNow; DateTime jobStartTimeUtc = DateTime.UtcNow;
_runnerSettings = HostContext.GetService<IConfigurationStore>().GetSettings();
IRunnerService server = null; IRunnerService server = null;
// add orchestration id to useragent for better correlation. // add orchestration id to useragent for better correlation.
@@ -49,13 +50,9 @@ namespace GitHub.Runner.Worker
!string.IsNullOrEmpty(orchestrationId.Value)) !string.IsNullOrEmpty(orchestrationId.Value))
{ {
HostContext.UserAgents.Add(new ProductInfoHeaderValue("OrchestrationId", orchestrationId.Value)); HostContext.UserAgents.Add(new ProductInfoHeaderValue("OrchestrationId", orchestrationId.Value));
}
var jobServerQueueTelemetry = false; // make sure orchestration id is in the user-agent header.
if (message.Variables.TryGetValue("DistributedTask.EnableJobServerQueueTelemetry", out VariableValue enableJobServerQueueTelemetry) && VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
!string.IsNullOrEmpty(enableJobServerQueueTelemetry?.Value))
{
jobServerQueueTelemetry = StringUtil.ConvertToBoolean(enableJobServerQueueTelemetry.Value);
} }
ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase)); ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
@@ -79,7 +76,7 @@ namespace GitHub.Runner.Worker
launchServer.InitializeLaunchClient(new Uri(launchReceiverEndpoint), accessToken); launchServer.InitializeLaunchClient(new Uri(launchReceiverEndpoint), accessToken);
} }
_jobServerQueue = HostContext.GetService<IJobServerQueue>(); _jobServerQueue = HostContext.GetService<IJobServerQueue>();
_jobServerQueue.Start(message, resultsServiceOnly: true, enableTelemetry: jobServerQueueTelemetry); _jobServerQueue.Start(message, resultsServiceOnly: true);
} }
else else
{ {
@@ -101,7 +98,7 @@ namespace GitHub.Runner.Worker
VssConnection jobConnection = VssUtil.CreateConnection(jobServerUrl, jobServerCredential, delegatingHandlers); VssConnection jobConnection = VssUtil.CreateConnection(jobServerUrl, jobServerCredential, delegatingHandlers);
await jobServer.ConnectAsync(jobConnection); await jobServer.ConnectAsync(jobConnection);
_jobServerQueue.Start(message, enableTelemetry: jobServerQueueTelemetry); _jobServerQueue.Start(message);
server = jobServer; server = jobServer;
} }
@@ -161,8 +158,6 @@ namespace GitHub.Runner.Worker
jobContext.SetRunnerContext("os", VarUtil.OS); jobContext.SetRunnerContext("os", VarUtil.OS);
jobContext.SetRunnerContext("arch", VarUtil.OSArchitecture); jobContext.SetRunnerContext("arch", VarUtil.OSArchitecture);
_runnerSettings = HostContext.GetService<IConfigurationStore>().GetSettings();
jobContext.SetRunnerContext("name", _runnerSettings.AgentName); jobContext.SetRunnerContext("name", _runnerSettings.AgentName);
if (jobContext.Global.Variables.TryGetValue(WellKnownDistributedTaskVariables.RunnerEnvironment, out var runnerEnvironment)) if (jobContext.Global.Variables.TryGetValue(WellKnownDistributedTaskVariables.RunnerEnvironment, out var runnerEnvironment))
@@ -295,6 +290,14 @@ namespace GitHub.Runner.Worker
jobContext.Warning(string.Format(Constants.Runner.EnforcedNode12DetectedAfterEndOfLife, actions)); jobContext.Warning(string.Format(Constants.Runner.EnforcedNode12DetectedAfterEndOfLife, actions));
} }
if (jobContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode16DetectedAfterEndOfLifeEnvVariable, out var node20ForceWarnings) && (jobContext.Global.Variables.GetBoolean("DistributedTask.ForceGithubJavascriptActionsToNode20") ?? false))
{
var actions = string.Join(", ", StringUtil.ConvertFromJson<HashSet<string>>(node20ForceWarnings));
jobContext.Warning(string.Format(Constants.Runner.EnforcedNode16DetectedAfterEndOfLife, actions));
}
await ShutdownQueue(throwOnFailure: false);
// Make sure to clean temp after file upload since they may be pending fileupload still use the TEMP dir. // Make sure to clean temp after file upload since they may be pending fileupload still use the TEMP dir.
_tempDirectoryManager?.CleanupTempDirectory(); _tempDirectoryManager?.CleanupTempDirectory();
@@ -400,6 +403,12 @@ namespace GitHub.Runner.Worker
jobContext.Warning(string.Format(Constants.Runner.EnforcedNode12DetectedAfterEndOfLife, actions)); jobContext.Warning(string.Format(Constants.Runner.EnforcedNode12DetectedAfterEndOfLife, actions));
} }
if (jobContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode16DetectedAfterEndOfLifeEnvVariable, out var node20ForceWarnings))
{
var actions = string.Join(", ", StringUtil.ConvertFromJson<HashSet<string>>(node20ForceWarnings));
jobContext.Warning(string.Format(Constants.Runner.EnforcedNode16DetectedAfterEndOfLife, actions));
}
try try
{ {
var jobQueueTelemetry = await ShutdownQueue(throwOnFailure: true); var jobQueueTelemetry = await ShutdownQueue(throwOnFailure: true);

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Worker
{
[ServiceLocator(Default = typeof(OSWarningChecker))]
public interface IOSWarningChecker : IRunnerService
{
Task CheckOSAsync(IExecutionContext context);
}
public sealed class OSWarningChecker : RunnerService, IOSWarningChecker
{
private static TimeSpan s_regexTimeout = TimeSpan.FromSeconds(1);
public async Task CheckOSAsync(IExecutionContext context)
{
ArgUtil.NotNull(context, nameof(context));
if (!context.Global.Variables.System_TestDotNet8Compatibility)
{
return;
}
context.Output("Testing runner upgrade compatibility");
List<string> output = new();
object outputLock = new();
try
{
using (var process = HostContext.CreateService<IProcessInvoker>())
{
process.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
{
if (!string.IsNullOrEmpty(stdout.Data))
{
lock (outputLock)
{
output.Add(stdout.Data);
Trace.Info(stdout.Data);
}
}
};
process.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
{
if (!string.IsNullOrEmpty(stderr.Data))
{
lock (outputLock)
{
output.Add(stderr.Data);
Trace.Error(stderr.Data);
}
}
};
using (var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
int exitCode = await process.ExecuteAsync(
workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
fileName: Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "testDotNet8Compatibility", $"TestDotNet8Compatibility{IOUtil.ExeExtension}"),
arguments: string.Empty,
environment: null,
cancellationToken: cancellationTokenSource.Token);
var outputStr = string.Join("\n", output).Trim();
if (exitCode != 0 || !string.Equals(outputStr, "Hello from .NET 8!", StringComparison.Ordinal))
{
var pattern = context.Global.Variables.System_DotNet8CompatibilityOutputPattern;
if (!string.IsNullOrEmpty(pattern))
{
var regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, s_regexTimeout);
if (!regex.IsMatch(outputStr))
{
return;
}
}
var warningMessage = context.Global.Variables.System_DotNet8CompatibilityWarning;
if (!string.IsNullOrEmpty(warningMessage))
{
context.Warning(warningMessage);
}
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.General, Message = $".NET 8 OS compatibility test failed with exit code '{exitCode}' and output: {GetShortOutput(context, output)}" });
}
}
}
}
catch (Exception ex)
{
Trace.Error("An error occurred while testing .NET 8 compatibility'");
Trace.Error(ex);
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.General, Message = $".NET 8 OS compatibility test encountered exception type '{ex.GetType().FullName}', message: '{ex.Message}', process output: '{GetShortOutput(context, output)}'" });
}
}
private static string GetShortOutput(IExecutionContext context, List<string> output)
{
var length = context.Global.Variables.System_DotNet8CompatibilityOutputLength ?? 200;
var outputStr = string.Join("\n", output).Trim();
return outputStr.Length > length ? string.Concat(outputStr.Substring(0, length), "[...]") : outputStr;
}
}
}

View File

@@ -0,0 +1,32 @@
#nullable enable
using System.IO;
using System.Threading.Tasks;
using GitHub.DistributedTask.Pipelines;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Worker;
[ServiceLocator(Default = typeof(SnapshotOperationProvider))]
public interface ISnapshotOperationProvider : IRunnerService
{
Task CreateSnapshotRequestAsync(IExecutionContext executionContext, Snapshot snapshotRequest);
}
public class SnapshotOperationProvider : RunnerService, ISnapshotOperationProvider
{
public Task CreateSnapshotRequestAsync(IExecutionContext executionContext, Snapshot snapshotRequest)
{
var snapshotRequestFilePath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), ".snapshot", "request.json");
var snapshotRequestDirectoryPath = Path.GetDirectoryName(snapshotRequestFilePath);
if (snapshotRequestDirectoryPath != null)
{
Directory.CreateDirectory(snapshotRequestDirectoryPath);
}
IOUtil.SaveObject(snapshotRequest, snapshotRequestFilePath);
executionContext.Output($"Request written to: {snapshotRequestFilePath}");
executionContext.Output("This request will be processed after the job completes. You will not receive any feedback on the snapshot process within the workflow logs of this job.");
executionContext.Output("If the snapshot process is successful, you should see a new image with the requested name in the list of available custom images when creating a new GitHub-hosted Runner.");
return Task.CompletedTask;
}
}

View File

@@ -295,7 +295,7 @@ namespace GitHub.Runner.Worker
!jobCancellationToken.IsCancellationRequested) !jobCancellationToken.IsCancellationRequested)
{ {
Trace.Error($"Caught timeout exception from step: {ex.Message}"); Trace.Error($"Caught timeout exception from step: {ex.Message}");
step.ExecutionContext.Error("The action has timed out."); step.ExecutionContext.Error($"The action '{step.DisplayName}' has timed out after {timeoutMinutes} minutes.");
step.ExecutionContext.Result = TaskResult.Failed; step.ExecutionContext.Result = TaskResult.Failed;
} }
else else

View File

@@ -72,8 +72,16 @@ namespace GitHub.Runner.Worker
public bool? Step_Debug => GetBoolean(Constants.Variables.Actions.StepDebug); public bool? Step_Debug => GetBoolean(Constants.Variables.Actions.StepDebug);
public string System_DotNet8CompatibilityWarning => Get(Constants.Variables.System.DotNet8CompatibilityWarning);
public string System_DotNet8CompatibilityOutputPattern => Get(Constants.Variables.System.DotNet8CompatibilityOutputPattern);
public int? System_DotNet8CompatibilityOutputLength => GetInt(Constants.Variables.System.DotNet8CompatibilityOutputLength);
public string System_PhaseDisplayName => Get(Constants.Variables.System.PhaseDisplayName); public string System_PhaseDisplayName => Get(Constants.Variables.System.PhaseDisplayName);
public bool System_TestDotNet8Compatibility => GetBoolean(Constants.Variables.System.TestDotNet8Compatibility) ?? false;
public string Get(string name) public string Get(string name)
{ {
Variable variable; Variable variable;

View File

@@ -1,12 +1,11 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace GitHub.Services.Common.Internal namespace GitHub.Services.Common.Internal
{ {
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public static class RawHttpHeaders public static class RawHttpHeaders
{ {
public const String SessionHeader = "X-Runner-Session"; public const String SessionHeader = "X-Actions-Session";
} }
} }

View File

@@ -138,6 +138,8 @@ namespace GitHub.Services.Common
response.Dispose(); response.Dispose();
} }
this.Settings.ApplyTo(request);
// Let's start with sending a token // Let's start with sending a token
IssuedToken token = null; IssuedToken token = null;
if (m_tokenProvider != null) if (m_tokenProvider != null)

View File

@@ -214,25 +214,7 @@ namespace GitHub.Services.Common
// ConfigureAwait(false) enables the continuation to be run outside any captured // ConfigureAwait(false) enables the continuation to be run outside any captured
// SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking... // SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking...
var tmpResponse = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false);
if (Settings.AllowAutoRedirectForBroker && tmpResponse.StatusCode == HttpStatusCode.Redirect)
{
//Dispose of the previous response
tmpResponse?.Dispose();
var location = tmpResponse.Headers.Location;
request = new HttpRequestMessage(HttpMethod.Get, location);
// Reapply the token to new redirected request
ApplyToken(request, token, applyICredentialsToWebProxy: lastResponseDemandedProxyAuth);
// Resend the request
response = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false); response = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false);
}
else
{
response = tmpResponse;
}
traceInfo?.TraceRequestSendTime(); traceInfo?.TraceRequestSendTime();

View File

@@ -110,16 +110,6 @@ namespace GitHub.Services.Common
set; set;
} }
/// <summary>
/// Gets or sets a value indicating whether or not HttpClientHandler should follow redirect on outgoing broker requests
/// This is special since this also sends token in the request, where as default AllowAutoRedirect does not
/// </summary>
public Boolean AllowAutoRedirectForBroker
{
get;
set;
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not compression should be used on outgoing requests. /// Gets or sets a value indicating whether or not compression should be used on outgoing requests.
/// The default value is true. /// The default value is true.

View File

@@ -43,6 +43,7 @@ namespace GitHub.DistributedTask.Pipelines
TemplateToken jobOutputs, TemplateToken jobOutputs,
IList<TemplateToken> defaults, IList<TemplateToken> defaults,
ActionsEnvironmentReference actionsEnvironment, ActionsEnvironmentReference actionsEnvironment,
TemplateToken snapshot,
String messageType = JobRequestMessageTypes.PipelineAgentJobRequest) String messageType = JobRequestMessageTypes.PipelineAgentJobRequest)
{ {
this.MessageType = messageType; this.MessageType = messageType;
@@ -57,6 +58,7 @@ namespace GitHub.DistributedTask.Pipelines
this.Workspace = workspaceOptions; this.Workspace = workspaceOptions;
this.JobOutputs = jobOutputs; this.JobOutputs = jobOutputs;
this.ActionsEnvironment = actionsEnvironment; this.ActionsEnvironment = actionsEnvironment;
this.Snapshot = snapshot;
m_variables = new Dictionary<String, VariableValue>(variables, StringComparer.OrdinalIgnoreCase); m_variables = new Dictionary<String, VariableValue>(variables, StringComparer.OrdinalIgnoreCase);
m_maskHints = new List<MaskHint>(maskHints); m_maskHints = new List<MaskHint>(maskHints);
m_steps = new List<JobStep>(steps); m_steps = new List<JobStep>(steps);
@@ -237,6 +239,13 @@ namespace GitHub.DistributedTask.Pipelines
set; set;
} }
[DataMember(EmitDefaultValue = false)]
public TemplateToken Snapshot
{
get;
set;
}
/// <summary> /// <summary>
/// Gets the collection of variables associated with the current context. /// Gets the collection of variables associated with the current context.
/// </summary> /// </summary>

View File

@@ -29,6 +29,8 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
public const String Id = "id"; public const String Id = "id";
public const String If = "if"; public const String If = "if";
public const String Image = "image"; public const String Image = "image";
public const String ImageName = "image-name";
public const String CustomImageVersion = "version";
public const String Include = "include"; public const String Include = "include";
public const String Inputs = "inputs"; public const String Inputs = "inputs";
public const String Job = "job"; public const String Job = "job";
@@ -60,6 +62,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
public const String Services = "services"; public const String Services = "services";
public const String Shell = "shell"; public const String Shell = "shell";
public const String Skipped = "skipped"; public const String Skipped = "skipped";
public const String Snapshot = "snapshot";
public const String StepEnv = "step-env"; public const String StepEnv = "step-env";
public const String StepIfResult = "step-if-result"; public const String StepIfResult = "step-if-result";
public const String StepWith = "step-with"; public const String StepWith = "step-with";

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.Linq; using System.Linq;
using GitHub.DistributedTask.Expressions2; using GitHub.DistributedTask.Expressions2;
using GitHub.DistributedTask.Expressions2.Sdk; using GitHub.DistributedTask.Expressions2.Sdk;
@@ -346,6 +347,70 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
return result; return result;
} }
internal static Snapshot ConvertToJobSnapshotRequest(TemplateContext context, TemplateToken token)
{
string imageName = null;
string version = "1.*";
string versionString = string.Empty;
var condition = $"{PipelineTemplateConstants.Success}()";
if (token is StringToken snapshotStringLiteral)
{
imageName = snapshotStringLiteral.Value;
}
else
{
var snapshotMapping = token.AssertMapping($"{PipelineTemplateConstants.Snapshot}");
foreach (var snapshotPropertyPair in snapshotMapping)
{
var propertyName = snapshotPropertyPair.Key.AssertString($"{PipelineTemplateConstants.Snapshot} key");
var propertyValue = snapshotPropertyPair.Value;
switch (propertyName.Value)
{
case PipelineTemplateConstants.ImageName:
imageName = snapshotPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Snapshot} {propertyName}").Value;
break;
case PipelineTemplateConstants.If:
condition = ConvertToIfCondition(context, propertyValue, false);
break;
case PipelineTemplateConstants.CustomImageVersion:
versionString = propertyValue.AssertString($"job {PipelineTemplateConstants.Snapshot} {PipelineTemplateConstants.CustomImageVersion}").Value;
version = IsSnapshotImageVersionValid(versionString) ? versionString : null;
break;
default:
propertyName.AssertUnexpectedValue($"{PipelineTemplateConstants.Snapshot} key");
break;
}
}
}
if (String.IsNullOrEmpty(imageName))
{
return null;
}
return new Snapshot(imageName)
{
Condition = condition,
Version = version
};
}
private static bool IsSnapshotImageVersionValid(string versionString)
{
var versionSegments = versionString.Split(".");
if (versionSegments.Length != 2 ||
!versionSegments[1].Equals("*") ||
!Int32.TryParse(versionSegments[0], NumberStyles.None, CultureInfo.InvariantCulture, result: out int parsedMajor) ||
parsedMajor < 0)
{
return false;
}
return true;
}
private static ActionStep ConvertToStep( private static ActionStep ConvertToStep(
TemplateContext context, TemplateContext context,
TemplateToken stepsItem, TemplateToken stepsItem,

View File

@@ -370,6 +370,32 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
return result; return result;
} }
public Snapshot EvaluateJobSnapshotRequest(TemplateToken token,
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
var result = default(Snapshot);
if (token != null && token.Type != TokenType.Null)
{
var context = CreateContext(contextData, expressionFunctions);
try
{
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Snapshot, token, 0, null, omitHeader: true);
context.Errors.Check();
result = PipelineTemplateConverter.ConvertToJobSnapshotRequest(context, token);
}
catch (Exception ex) when (!(ex is TemplateValidationException))
{
context.Errors.Add(ex);
}
context.Errors.Check();
}
return result;
}
private TemplateContext CreateContext( private TemplateContext CreateContext(
DictionaryContextData contextData, DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions, IList<IFunctionInfo> expressionFunctions,

View File

@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
namespace GitHub.DistributedTask.Pipelines
{
[DataContract]
public class Snapshot
{
public Snapshot(string imageName, string condition = null, string version = null)
{
ImageName = imageName;
Condition = condition ?? $"{PipelineTemplateConstants.Success}()";
Version = version ?? "1.*";
}
[DataMember(EmitDefaultValue = false)]
public String ImageName { get; set; }
[DataMember(EmitDefaultValue = false)]
public String Condition { get; set; }
[DataMember(EmitDefaultValue = false)]
public String Version { get; set; }
}
}

View File

@@ -71,7 +71,8 @@
"env": "job-env", "env": "job-env",
"outputs": "job-outputs", "outputs": "job-outputs",
"defaults": "job-defaults", "defaults": "job-defaults",
"steps": "steps" "steps": "steps",
"snapshot": "snapshot"
} }
} }
}, },
@@ -155,6 +156,41 @@
} }
}, },
"snapshot": {
"one-of": [
"non-empty-string",
"snapshot-mapping"
]
},
"snapshot-mapping": {
"mapping": {
"properties": {
"image-name": {
"type": "non-empty-string",
"required": true
},
"if": "snapshot-if",
"version": {
"type": "non-empty-string",
"required": false
}
}
}
},
"snapshot-if": {
"context": [
"github",
"inputs",
"vars",
"needs",
"strategy",
"matrix"
],
"string": {}
},
"runs-on": { "runs-on": {
"context": [ "context": [
"github", "github",

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Services.Common;
using GitHub.Services.Common.Diagnostics;
using GitHub.Services.WebApi;
using Newtonsoft.Json;
namespace GitHub.DistributedTask.WebApi
{
[ResourceArea(TaskResourceIds.AreaId)]
public class ActionsRunServerHttpClient : TaskAgentHttpClient
{
private static readonly JsonSerializerSettings s_serializerSettings;
static ActionsRunServerHttpClient()
{
s_serializerSettings = new VssJsonMediaTypeFormatter().SerializerSettings;
s_serializerSettings.DateParseHandling = DateParseHandling.None;
s_serializerSettings.FloatParseHandling = FloatParseHandling.Double;
}
public ActionsRunServerHttpClient(
Uri baseUrl,
VssCredentials credentials)
: base(baseUrl, credentials)
{
}
public ActionsRunServerHttpClient(
Uri baseUrl,
VssCredentials credentials,
VssHttpRequestSettings settings)
: base(baseUrl, credentials, settings)
{
}
public ActionsRunServerHttpClient(
Uri baseUrl,
VssCredentials credentials,
params DelegatingHandler[] handlers)
: base(baseUrl, credentials, handlers)
{
}
public ActionsRunServerHttpClient(
Uri baseUrl,
VssCredentials credentials,
VssHttpRequestSettings settings,
params DelegatingHandler[] handlers)
: base(baseUrl, credentials, settings, handlers)
{
}
public ActionsRunServerHttpClient(
Uri baseUrl,
HttpMessageHandler pipeline,
Boolean disposeHandler)
: base(baseUrl, pipeline, disposeHandler)
{
}
public Task<Pipelines.AgentJobRequestMessage> GetJobMessageAsync(
string messageId,
object userState = null,
CancellationToken cancellationToken = default)
{
HttpMethod httpMethod = new HttpMethod("GET");
Guid locationId = new Guid("25adab70-1379-4186-be8e-b643061ebe3a");
object routeValues = new { messageId = messageId };
return SendAsync<Pipelines.AgentJobRequestMessage>(
httpMethod,
locationId,
routeValues: routeValues,
version: new ApiResourceVersion(6.0, 1),
userState: userState,
cancellationToken: cancellationToken);
}
protected override async Task<T> ReadJsonContentAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken = default(CancellationToken))
{
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
return JsonConvert.DeserializeObject<T>(json, s_serializerSettings);
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Runtime.Serialization;
namespace GitHub.DistributedTask.WebApi
{
/// <summary>
/// Message that tells the runner to redirect itself to BrokerListener for messages.
/// (Note that we use a special Message instead of a simple 302. This is because
/// the runner will need to apply the runner's token to the request, and it is
/// a security best practice to *not* blindly add sensitive data to redirects
/// 302s.)
/// </summary>
[DataContract]
public class BrokerMigrationMessage
{
public static readonly string MessageType = "BrokerMigration";
public BrokerMigrationMessage()
{
}
public BrokerMigrationMessage(
Uri brokerUrl)
{
this.BrokerBaseUrl = brokerUrl;
}
/// <summary>
/// The base url for the broker listener
/// </summary>
[DataMember]
public Uri BrokerBaseUrl
{
get;
internal set;
}
}
}

View File

@@ -1539,6 +1539,26 @@ namespace GitHub.DistributedTask.WebApi
} }
} }
[Serializable]
[ExceptionMapping("0.0", "3.0", "TaskOrchestrationJobUnprocessableException", "GitHub.DistributedTask.WebApi.TaskOrchestrationJobUnprocessableException, GitHub.DistributedTask.WebApi, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public sealed class TaskOrchestrationJobUnprocessableException : DistributedTaskException
{
public TaskOrchestrationJobUnprocessableException(String message)
: base(message)
{
}
public TaskOrchestrationJobUnprocessableException(String message, Exception innerException)
: base(message, innerException)
{
}
private TaskOrchestrationJobUnprocessableException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
[Serializable] [Serializable]
[ExceptionMapping("0.0", "3.0", "TaskOrchestrationPlanSecurityException", "GitHub.DistributedTask.WebApi.TaskOrchestrationPlanSecurityException, GitHub.DistributedTask.WebApi, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")] [ExceptionMapping("0.0", "3.0", "TaskOrchestrationPlanSecurityException", "GitHub.DistributedTask.WebApi.TaskOrchestrationPlanSecurityException, GitHub.DistributedTask.WebApi, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public sealed class TaskOrchestrationPlanSecurityException : DistributedTaskException public sealed class TaskOrchestrationPlanSecurityException : DistributedTaskException
@@ -2498,6 +2518,25 @@ namespace GitHub.DistributedTask.WebApi
} }
} }
[Serializable]
public class NonRetryableActionDownloadInfoException : DistributedTaskException
{
public NonRetryableActionDownloadInfoException(String message)
: base(message)
{
}
public NonRetryableActionDownloadInfoException(String message, Exception innerException)
: base(message, innerException)
{
}
protected NonRetryableActionDownloadInfoException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
[Serializable] [Serializable]
public sealed class FailedToResolveActionDownloadInfoException : DistributedTaskException public sealed class FailedToResolveActionDownloadInfoException : DistributedTaskException
{ {

View File

@@ -141,24 +141,6 @@ namespace GitHub.DistributedTask.WebApi
return ReplaceAgentAsync(poolId, agent.Id, agent, userState, cancellationToken); return ReplaceAgentAsync(poolId, agent.Id, agent, userState, cancellationToken);
} }
public Task<Pipelines.AgentJobRequestMessage> GetJobMessageAsync(
string messageId,
object userState = null,
CancellationToken cancellationToken = default)
{
HttpMethod httpMethod = new HttpMethod("GET");
Guid locationId = new Guid("25adab70-1379-4186-be8e-b643061ebe3a");
object routeValues = new { messageId = messageId };
return SendAsync<Pipelines.AgentJobRequestMessage>(
httpMethod,
locationId,
routeValues: routeValues,
version: new ApiResourceVersion(6.0, 1),
userState: userState,
cancellationToken: cancellationToken);
}
protected Task<T> SendAsync<T>( protected Task<T> SendAsync<T>(
HttpMethod method, HttpMethod method,
Guid locationId, Guid locationId,

View File

@@ -0,0 +1,10 @@
using System;
using System.Runtime.Serialization;
namespace GitHub.DistributedTask.WebApi
{
public sealed class TaskAgentMessageTypes
{
public static readonly string ForceTokenRefresh = "ForceTokenRefresh";
}
}

View File

@@ -75,5 +75,12 @@ namespace GitHub.DistributedTask.WebApi
get; get;
set; set;
} }
[DataMember(EmitDefaultValue = false, IsRequired = false)]
public BrokerMigrationMessage BrokerMigrationMessage
{
get;
set;
}
} }
} }

View File

@@ -101,6 +101,7 @@ namespace GitHub.DistributedTask.WebApi
public static readonly String FileAttachment = "DistributedTask.Core.FileAttachment"; public static readonly String FileAttachment = "DistributedTask.Core.FileAttachment";
public static readonly String DiagnosticLog = "DistributedTask.Core.DiagnosticLog"; public static readonly String DiagnosticLog = "DistributedTask.Core.DiagnosticLog";
public static readonly String ResultsLog = "Results.Core.Log"; public static readonly String ResultsLog = "Results.Core.Log";
public static readonly String ResultsDiagnosticLog = "Results.Core.DiagnosticLog";
} }
[GenerateAllConstants] [GenerateAllConstants]

View File

@@ -7,5 +7,8 @@ namespace GitHub.Actions.RunService.WebApi
{ {
[DataMember(Name = "jobMessageId", EmitDefaultValue = false)] [DataMember(Name = "jobMessageId", EmitDefaultValue = false)]
public string JobMessageId { get; set; } public string JobMessageId { get; set; }
[DataMember(Name = "runnerOS", EmitDefaultValue = false)]
public string RunnerOS { get; set; }
} }
} }

View File

@@ -0,0 +1,20 @@
using System.Runtime.Serialization;
namespace GitHub.Actions.RunService.WebApi
{
[DataContract]
public class BrokerError
{
[DataMember(Name = "source", EmitDefaultValue = false)]
public string Source { get; set; }
[DataMember(Name = "errorKind", EmitDefaultValue = false)]
public string ErrorKind { get; set; }
[DataMember(Name = "statusCode", EmitDefaultValue = false)]
public int StatusCode { get; set; }
[DataMember(Name = "errorMessage", EmitDefaultValue = false)]
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using System.Runtime.Serialization;
namespace GitHub.Actions.RunService.WebApi
{
[DataContract]
public class BrokerErrorKind
{
public const string RunnerVersionTooOld = "RunnerVersionTooOld";
}
}

View File

@@ -0,0 +1,17 @@
using System.Runtime.Serialization;
namespace GitHub.Actions.RunService.WebApi
{
[DataContract]
public class RunServiceError
{
[DataMember(Name = "source", EmitDefaultValue = false)]
public string Source { get; set; }
[DataMember(Name = "statusCode", EmitDefaultValue = false)]
public int Code { get; set; }
[DataMember(Name = "errorMessage", EmitDefaultValue = false)]
public string Message { get; set; }
}
}

View File

@@ -9,6 +9,7 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Services.Common; using GitHub.Services.Common;
using GitHub.Services.OAuth; using GitHub.Services.OAuth;
using GitHub.Services.WebApi; using GitHub.Services.WebApi;
using Newtonsoft.Json;
using Sdk.RSWebApi.Contracts; using Sdk.RSWebApi.Contracts;
using Sdk.WebApi.WebApi; using Sdk.WebApi.WebApi;
@@ -16,6 +17,15 @@ namespace GitHub.Actions.RunService.WebApi
{ {
public class RunServiceHttpClient : RawHttpClientBase public class RunServiceHttpClient : RawHttpClientBase
{ {
private static readonly JsonSerializerSettings s_serializerSettings;
static RunServiceHttpClient()
{
s_serializerSettings = new VssJsonMediaTypeFormatter().SerializerSettings;
s_serializerSettings.DateParseHandling = DateParseHandling.None;
s_serializerSettings.FloatParseHandling = FloatParseHandling.Double;
}
public RunServiceHttpClient( public RunServiceHttpClient(
Uri baseUrl, Uri baseUrl,
VssOAuthCredential credentials) VssOAuthCredential credentials)
@@ -59,12 +69,14 @@ namespace GitHub.Actions.RunService.WebApi
public async Task<AgentJobRequestMessage> GetJobMessageAsync( public async Task<AgentJobRequestMessage> GetJobMessageAsync(
Uri requestUri, Uri requestUri,
string messageId, string messageId,
string runnerOS,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
HttpMethod httpMethod = new HttpMethod("POST"); HttpMethod httpMethod = new HttpMethod("POST");
var payload = new AcquireJobRequest var payload = new AcquireJobRequest
{ {
JobMessageId = messageId, JobMessageId = messageId,
RunnerOS = runnerOS
}; };
requestUri = new Uri(requestUri, "acquirejob"); requestUri = new Uri(requestUri, "acquirejob");
@@ -74,6 +86,7 @@ namespace GitHub.Actions.RunService.WebApi
httpMethod, httpMethod,
requestUri: requestUri, requestUri: requestUri,
content: requestContent, content: requestContent,
readErrorBody: true,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
if (result.IsSuccess) if (result.IsSuccess)
@@ -81,13 +94,25 @@ namespace GitHub.Actions.RunService.WebApi
return result.Value; return result.Value;
} }
switch (result.StatusCode) if (TryParseErrorBody(result.ErrorBody, out RunServiceError error))
{
switch ((HttpStatusCode)error.Code)
{ {
case HttpStatusCode.NotFound: case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job message not found: {messageId}"); throw new TaskOrchestrationJobNotFoundException($"Job message not found '{messageId}'. {error.Message}");
case HttpStatusCode.Conflict: case HttpStatusCode.Conflict:
throw new TaskOrchestrationJobAlreadyAcquiredException($"Job message already acquired: {messageId}"); throw new TaskOrchestrationJobAlreadyAcquiredException($"Job message already acquired '{messageId}'. {error.Message}");
default: case HttpStatusCode.UnprocessableEntity:
throw new TaskOrchestrationJobUnprocessableException($"Unprocessable job '{messageId}'. {error.Message}");
}
}
if (!string.IsNullOrEmpty(result.ErrorBody))
{
throw new Exception($"Failed to get job message: {result.Error}. {Truncate(result.ErrorBody)}");
}
else
{
throw new Exception($"Failed to get job message: {result.Error}"); throw new Exception($"Failed to get job message: {result.Error}");
} }
} }
@@ -96,7 +121,7 @@ namespace GitHub.Actions.RunService.WebApi
Uri requestUri, Uri requestUri,
Guid planId, Guid planId,
Guid jobId, Guid jobId,
TaskResult result, TaskResult conclusion,
Dictionary<String, VariableValue> outputs, Dictionary<String, VariableValue> outputs,
IList<StepResult> stepResults, IList<StepResult> stepResults,
IList<Annotation> jobAnnotations, IList<Annotation> jobAnnotations,
@@ -108,7 +133,7 @@ namespace GitHub.Actions.RunService.WebApi
{ {
PlanID = planId, PlanID = planId,
JobID = jobId, JobID = jobId,
Conclusion = result, Conclusion = conclusion,
Outputs = outputs, Outputs = outputs,
StepResults = stepResults, StepResults = stepResults,
Annotations = jobAnnotations, Annotations = jobAnnotations,
@@ -118,22 +143,32 @@ namespace GitHub.Actions.RunService.WebApi
requestUri = new Uri(requestUri, "completejob"); requestUri = new Uri(requestUri, "completejob");
var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true)); var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
var response = await SendAsync( var result = await Send2Async(
httpMethod, httpMethod,
requestUri, requestUri,
content: requestContent, content: requestContent,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode) if (result.IsSuccess)
{ {
return; return;
} }
switch (response.StatusCode) if (TryParseErrorBody(result.ErrorBody, out RunServiceError error))
{
switch ((HttpStatusCode)error.Code)
{ {
case HttpStatusCode.NotFound: case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}"); throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}. {error.Message}");
default: }
throw new Exception($"Failed to complete job: {response.ReasonPhrase}"); }
if (!string.IsNullOrEmpty(result.ErrorBody))
{
throw new Exception($"Failed to complete job: {result.Error}. {Truncate(result.ErrorBody)}");
}
else
{
throw new Exception($"Failed to complete job: {result.Error}");
} }
} }
@@ -157,6 +192,7 @@ namespace GitHub.Actions.RunService.WebApi
httpMethod, httpMethod,
requestUri, requestUri,
content: requestContent, content: requestContent,
readErrorBody: true,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
if (result.IsSuccess) if (result.IsSuccess)
@@ -164,13 +200,60 @@ namespace GitHub.Actions.RunService.WebApi
return result.Value; return result.Value;
} }
switch (result.StatusCode) if (TryParseErrorBody(result.ErrorBody, out RunServiceError error))
{
switch ((HttpStatusCode)error.Code)
{ {
case HttpStatusCode.NotFound: case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}"); throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}. {error.Message}");
default: }
}
if (!string.IsNullOrEmpty(result.ErrorBody))
{
throw new Exception($"Failed to renew job: {result.Error}. {Truncate(result.ErrorBody)}");
}
else
{
throw new Exception($"Failed to renew job: {result.Error}"); throw new Exception($"Failed to renew job: {result.Error}");
} }
} }
protected override async Task<T> ReadJsonContentAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken = default(CancellationToken))
{
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
return JsonConvert.DeserializeObject<T>(json, s_serializerSettings);
}
private static bool TryParseErrorBody(string errorBody, out RunServiceError error)
{
if (!string.IsNullOrEmpty(errorBody))
{
try
{
error = JsonUtility.FromString<RunServiceError>(errorBody);
if (error?.Source == "actions-run-service")
{
return true;
}
}
catch (Exception)
{
}
}
error = null;
return false;
}
private static string Truncate(string errorBody)
{
if (errorBody.Length > 100)
{
return errorBody.Substring(0, 100) + "[truncated]";
}
return errorBody;
}
} }
} }

View File

@@ -57,6 +57,7 @@ namespace GitHub.Actions.RunService.WebApi
} }
public async Task<TaskAgentMessage> GetRunnerMessageAsync( public async Task<TaskAgentMessage> GetRunnerMessageAsync(
Guid? sessionId,
string runnerVersion, string runnerVersion,
TaskAgentStatus? status, TaskAgentStatus? status,
string os = null, string os = null,
@@ -69,6 +70,11 @@ namespace GitHub.Actions.RunService.WebApi
List<KeyValuePair<string, string>> queryParams = new List<KeyValuePair<string, string>>(); List<KeyValuePair<string, string>> queryParams = new List<KeyValuePair<string, string>>();
if (sessionId != null)
{
queryParams.Add("sessionId", sessionId.Value.ToString());
}
if (status != null) if (status != null)
{ {
queryParams.Add("status", status.Value.ToString()); queryParams.Add("status", status.Value.ToString());
@@ -97,6 +103,52 @@ namespace GitHub.Actions.RunService.WebApi
new HttpMethod("GET"), new HttpMethod("GET"),
requestUri: requestUri, requestUri: requestUri,
queryParameters: queryParams, queryParameters: queryParams,
readErrorBody: true,
cancellationToken: cancellationToken);
if (result.IsSuccess)
{
return result.Value;
}
if (TryParseErrorBody(result.ErrorBody, out BrokerError brokerError))
{
switch (brokerError.ErrorKind)
{
case BrokerErrorKind.RunnerVersionTooOld:
throw new AccessDeniedException(brokerError.Message)
{
ErrorCode = 1
};
default:
break;
}
}
// temporary back compat
if (result.StatusCode == HttpStatusCode.Forbidden)
{
throw new AccessDeniedException($"{result.Error} Runner version v{runnerVersion} is deprecated and cannot receive messages.")
{
ErrorCode = 1
};
}
throw new Exception($"Failed to get job message. Request to {requestUri} failed with status: {result.StatusCode}. Error message {result.Error}");
}
public async Task<TaskAgentSession> CreateSessionAsync(
TaskAgentSession session,
CancellationToken cancellationToken = default)
{
var requestUri = new Uri(Client.BaseAddress, "session");
var requestContent = new ObjectContent<TaskAgentSession>(session, new VssJsonMediaTypeFormatter(true));
var result = await SendAsync<TaskAgentSession>(
new HttpMethod("POST"),
requestUri: requestUri,
content: requestContent,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
if (result.IsSuccess) if (result.IsSuccess)
@@ -109,7 +161,51 @@ namespace GitHub.Actions.RunService.WebApi
throw new AccessDeniedException(result.Error); throw new AccessDeniedException(result.Error);
} }
throw new Exception($"Failed to get job message: {result.Error}"); if (result.StatusCode == HttpStatusCode.Conflict)
{
throw new TaskAgentSessionConflictException(result.Error);
}
throw new Exception($"Failed to create broker session: {result.Error}");
}
public async Task DeleteSessionAsync(
CancellationToken cancellationToken = default)
{
var requestUri = new Uri(Client.BaseAddress, $"session");
var result = await SendAsync<object>(
new HttpMethod("DELETE"),
requestUri: requestUri,
cancellationToken: cancellationToken);
if (result.IsSuccess)
{
return;
}
throw new Exception($"Failed to delete broker session: {result.Error}");
}
private static bool TryParseErrorBody(string errorBody, out BrokerError error)
{
if (!string.IsNullOrEmpty(errorBody))
{
try
{
error = JsonUtility.FromString<BrokerError>(errorBody);
if (error?.Source == "actions-broker-listener")
{
return true;
}
}
catch (Exception)
{
}
}
error = null;
return false;
} }
} }
} }

View File

@@ -89,6 +89,26 @@ namespace GitHub.Services.Results.Contracts
public long SoftSizeLimit; public long SoftSizeLimit;
} }
[DataContract]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class GetSignedDiagnosticLogsURLRequest
{
[DataMember]
public string WorkflowJobRunBackendId;
[DataMember]
public string WorkflowRunBackendId;
}
[DataContract]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class GetSignedDiagnosticLogsURLResponse
{
[DataMember]
public string DiagLogsURL;
[DataMember]
public string BlobStorageType;
}
[DataContract] [DataContract]
[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class JobLogsMetadataCreate public class JobLogsMetadataCreate

View File

@@ -101,7 +101,7 @@ namespace Sdk.WebApi.WebApi
} }
} }
protected Task<RawHttpClientResult<T>> SendAsync<T>( protected async Task<RawHttpClientResult> Send2Async(
HttpMethod method, HttpMethod method,
Uri requestUri, Uri requestUri,
HttpContent content = null, HttpContent content = null,
@@ -109,7 +109,47 @@ namespace Sdk.WebApi.WebApi
Object userState = null, Object userState = null,
CancellationToken cancellationToken = default(CancellationToken)) CancellationToken cancellationToken = default(CancellationToken))
{ {
return SendAsync<T>(method, null, requestUri, content, queryParameters, userState, cancellationToken); using (var response = await SendAsync(method, requestUri, content, queryParameters, userState, cancellationToken).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
return new RawHttpClientResult(
isSuccess: true,
error: string.Empty,
statusCode: response.StatusCode);
}
else
{
var errorBody = default(string);
try
{
errorBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
errorBody = $"Error reading HTTP response body: {ex.Message}";
}
string errorMessage = $"Error: {response.ReasonPhrase}";
return new RawHttpClientResult(
isSuccess: false,
error: errorMessage,
statusCode: response.StatusCode,
errorBody: errorBody);
}
}
}
protected Task<RawHttpClientResult<T>> SendAsync<T>(
HttpMethod method,
Uri requestUri,
HttpContent content = null,
IEnumerable<KeyValuePair<String, String>> queryParameters = null,
Boolean readErrorBody = false,
Object userState = null,
CancellationToken cancellationToken = default(CancellationToken))
{
return SendAsync<T>(method, null, requestUri, content, queryParameters, readErrorBody, userState, cancellationToken);
} }
protected async Task<RawHttpClientResult<T>> SendAsync<T>( protected async Task<RawHttpClientResult<T>> SendAsync<T>(
@@ -118,18 +158,20 @@ namespace Sdk.WebApi.WebApi
Uri requestUri, Uri requestUri,
HttpContent content = null, HttpContent content = null,
IEnumerable<KeyValuePair<String, String>> queryParameters = null, IEnumerable<KeyValuePair<String, String>> queryParameters = null,
Boolean readErrorBody = false,
Object userState = null, Object userState = null,
CancellationToken cancellationToken = default(CancellationToken)) CancellationToken cancellationToken = default(CancellationToken))
{ {
using (VssTraceActivity.GetOrCreate().EnterCorrelationScope()) using (VssTraceActivity.GetOrCreate().EnterCorrelationScope())
using (HttpRequestMessage requestMessage = CreateRequestMessage(method, additionalHeaders, requestUri, content, queryParameters)) using (HttpRequestMessage requestMessage = CreateRequestMessage(method, additionalHeaders, requestUri, content, queryParameters))
{ {
return await SendAsync<T>(requestMessage, userState, cancellationToken).ConfigureAwait(false); return await SendAsync<T>(requestMessage, readErrorBody, userState, cancellationToken).ConfigureAwait(false);
} }
} }
protected async Task<RawHttpClientResult<T>> SendAsync<T>( protected async Task<RawHttpClientResult<T>> SendAsync<T>(
HttpRequestMessage message, HttpRequestMessage message,
Boolean readErrorBody = false,
Object userState = null, Object userState = null,
CancellationToken cancellationToken = default(CancellationToken)) CancellationToken cancellationToken = default(CancellationToken))
{ {
@@ -145,8 +187,21 @@ namespace Sdk.WebApi.WebApi
} }
else else
{ {
var errorBody = default(string);
if (readErrorBody)
{
try
{
errorBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
errorBody = $"Error reading HTTP response body: {ex.Message}";
}
}
string errorMessage = $"Error: {response.ReasonPhrase}"; string errorMessage = $"Error: {response.ReasonPhrase}";
return RawHttpClientResult<T>.Fail(errorMessage, response.StatusCode); return RawHttpClientResult<T>.Fail(errorMessage, response.StatusCode, errorBody);
} }
} }
} }

View File

@@ -5,15 +5,27 @@ namespace Sdk.WebApi.WebApi
public class RawHttpClientResult public class RawHttpClientResult
{ {
public bool IsSuccess { get; protected set; } public bool IsSuccess { get; protected set; }
/// <summary>
/// A description of the HTTP status code, like "Error: Unprocessable Entity"
/// </summary>
public string Error { get; protected set; } public string Error { get; protected set; }
/// <summary>
/// The HTTP response body for unsuccessful HTTP status codes, or an error message when reading the response body fails.
/// </summary>
public string ErrorBody { get; protected set; }
public HttpStatusCode StatusCode { get; protected set; } public HttpStatusCode StatusCode { get; protected set; }
public bool IsFailure => !IsSuccess; public bool IsFailure => !IsSuccess;
protected RawHttpClientResult(bool isSuccess, string error, HttpStatusCode statusCode) public RawHttpClientResult(bool isSuccess, string error, HttpStatusCode statusCode, string errorBody = null)
{ {
IsSuccess = isSuccess; IsSuccess = isSuccess;
Error = error; Error = error;
StatusCode = statusCode; StatusCode = statusCode;
ErrorBody = errorBody;
} }
} }
@@ -21,13 +33,13 @@ namespace Sdk.WebApi.WebApi
{ {
public T Value { get; private set; } public T Value { get; private set; }
protected internal RawHttpClientResult(T value, bool isSuccess, string error, HttpStatusCode statusCode) protected internal RawHttpClientResult(T value, bool isSuccess, string error, HttpStatusCode statusCode, string errorBody)
: base(isSuccess, error, statusCode) : base(isSuccess, error, statusCode, errorBody)
{ {
Value = value; Value = value;
} }
public static RawHttpClientResult<T> Fail(string message, HttpStatusCode statusCode) => new RawHttpClientResult<T>(default(T), false, message, statusCode); public static RawHttpClientResult<T> Fail(string message, HttpStatusCode statusCode, string errorBody) => new RawHttpClientResult<T>(default(T), false, message, statusCode, errorBody);
public static RawHttpClientResult<T> Ok(T value) => new RawHttpClientResult<T>(value, true, string.Empty, HttpStatusCode.OK); public static RawHttpClientResult<T> Ok(T value) => new RawHttpClientResult<T>(value, true, string.Empty, HttpStatusCode.OK, null);
} }
} }

View File

@@ -81,6 +81,19 @@ namespace GitHub.Services.Results.Client
return await GetResultsSignedURLResponse<GetSignedStepLogsURLRequest, GetSignedStepLogsURLResponse>(getStepLogsSignedBlobURLEndpoint, cancellationToken, request); return await GetResultsSignedURLResponse<GetSignedStepLogsURLRequest, GetSignedStepLogsURLResponse>(getStepLogsSignedBlobURLEndpoint, cancellationToken, request);
} }
private async Task<GetSignedDiagnosticLogsURLResponse> GetDiagnosticLogsUploadUrlAsync(string planId, string jobId, CancellationToken cancellationToken)
{
var request = new GetSignedDiagnosticLogsURLRequest()
{
WorkflowJobRunBackendId = jobId,
WorkflowRunBackendId = planId,
};
var getDiagnosticLogsSignedBlobURLEndpoint = new Uri(m_resultsServiceUrl, Constants.GetJobDiagLogsSignedBlobURL);
return await GetResultsSignedURLResponse<GetSignedDiagnosticLogsURLRequest, GetSignedDiagnosticLogsURLResponse>(getDiagnosticLogsSignedBlobURLEndpoint, cancellationToken, request);
}
private async Task<GetSignedJobLogsURLResponse> GetJobLogUploadUrlAsync(string planId, string jobId, CancellationToken cancellationToken) private async Task<GetSignedJobLogsURLResponse> GetJobLogUploadUrlAsync(string planId, string jobId, CancellationToken cancellationToken)
{ {
var request = new GetSignedJobLogsURLRequest() var request = new GetSignedJobLogsURLRequest()
@@ -209,14 +222,34 @@ namespace GitHub.Services.Results.Client
return new AppendBlobClient(blobUri.path, new AzureSasCredential(blobUri.sas), opts); return new AppendBlobClient(blobUri.path, new AzureSasCredential(blobUri.sas), opts);
} }
private async Task UploadBlockFileAsync(string url, string blobStorageType, FileStream file, CancellationToken cancellationToken) private async Task UploadBlockFileAsync(string url, string blobStorageType, FileStream file, CancellationToken cancellationToken, Dictionary<string, string> customHeaders = null)
{ {
if (m_useSdk && blobStorageType == BlobStorageTypes.AzureBlobStorage) if (m_useSdk && blobStorageType == BlobStorageTypes.AzureBlobStorage)
{ {
var blobClient = GetBlobClient(url); var blobClient = GetBlobClient(url);
var httpHeaders = new BlobHttpHeaders();
if (customHeaders != null)
{
foreach (var header in customHeaders)
{
switch (header.Key)
{
case Constants.ContentTypeHeader:
httpHeaders.ContentType = header.Value;
break;
}
}
}
try try
{ {
await blobClient.UploadAsync(file, cancellationToken); await blobClient.UploadAsync(file, new BlobUploadOptions()
{
HttpHeaders = httpHeaders,
Conditions = new BlobRequestConditions
{
IfNoneMatch = new ETag("*")
}
}, cancellationToken);
} }
catch (RequestFailedException e) catch (RequestFailedException e)
{ {
@@ -236,6 +269,14 @@ namespace GitHub.Services.Results.Client
request.Content.Headers.Add(Constants.AzureBlobTypeHeader, Constants.AzureBlockBlob); request.Content.Headers.Add(Constants.AzureBlobTypeHeader, Constants.AzureBlockBlob);
} }
if (customHeaders != null)
{
foreach (var header in customHeaders)
{
request.Content.Headers.Add(header.Key, header.Value);
}
};
using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken)) using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken))
{ {
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
@@ -246,14 +287,34 @@ namespace GitHub.Services.Results.Client
} }
} }
private async Task CreateAppendFileAsync(string url, string blobStorageType, CancellationToken cancellationToken) private async Task CreateAppendFileAsync(string url, string blobStorageType, CancellationToken cancellationToken, Dictionary<string, string> customHeaders = null)
{ {
if (m_useSdk && blobStorageType == BlobStorageTypes.AzureBlobStorage) if (m_useSdk && blobStorageType == BlobStorageTypes.AzureBlobStorage)
{ {
var appendBlobClient = GetAppendBlobClient(url); var appendBlobClient = GetAppendBlobClient(url);
var httpHeaders = new BlobHttpHeaders();
if (customHeaders != null)
{
foreach (var header in customHeaders)
{
switch (header.Key)
{
case Constants.ContentTypeHeader:
httpHeaders.ContentType = header.Value;
break;
}
}
}
try try
{ {
await appendBlobClient.CreateAsync(cancellationToken: cancellationToken); await appendBlobClient.CreateAsync(new AppendBlobCreateOptions()
{
HttpHeaders = httpHeaders,
Conditions = new AppendBlobRequestConditions
{
IfNoneMatch = new ETag("*")
}
}, cancellationToken: cancellationToken);
} }
catch (RequestFailedException e) catch (RequestFailedException e)
{ {
@@ -271,6 +332,13 @@ namespace GitHub.Services.Results.Client
request.Content.Headers.Add(Constants.AzureBlobTypeHeader, Constants.AzureAppendBlob); request.Content.Headers.Add(Constants.AzureBlobTypeHeader, Constants.AzureAppendBlob);
request.Content.Headers.Add("Content-Length", "0"); request.Content.Headers.Add("Content-Length", "0");
} }
if (customHeaders != null)
{
foreach (var header in customHeaders)
{
request.Content.Headers.Add(header.Key, header.Value);
}
};
using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken)) using (var response = await SendAsync(request, HttpCompletionOption.ResponseHeadersRead, userState: null, cancellationToken))
{ {
@@ -353,14 +421,14 @@ namespace GitHub.Services.Results.Client
} }
private async Task UploadLogFile(string file, bool finalize, bool firstBlock, string sasUrl, string blobStorageType, private async Task UploadLogFile(string file, bool finalize, bool firstBlock, string sasUrl, string blobStorageType,
CancellationToken cancellationToken) CancellationToken cancellationToken, Dictionary<string, string> customHeaders = null)
{ {
if (firstBlock && finalize) if (firstBlock && finalize)
{ {
// This is the one and only block, just use a block blob // This is the one and only block, just use a block blob
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true)) using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
{ {
await UploadBlockFileAsync(sasUrl, blobStorageType, fileStream, cancellationToken); await UploadBlockFileAsync(sasUrl, blobStorageType, fileStream, cancellationToken, customHeaders);
} }
} }
else else
@@ -369,7 +437,7 @@ namespace GitHub.Services.Results.Client
// Create the Append blob // Create the Append blob
if (firstBlock) if (firstBlock)
{ {
await CreateAppendFileAsync(sasUrl, blobStorageType, cancellationToken); await CreateAppendFileAsync(sasUrl, blobStorageType, cancellationToken, customHeaders);
} }
// Upload content // Upload content
@@ -391,7 +459,12 @@ namespace GitHub.Services.Results.Client
throw new Exception("Failed to get step log upload url"); throw new Exception("Failed to get step log upload url");
} }
await UploadLogFile(file, finalize, firstBlock, uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, cancellationToken); var customHeaders = new Dictionary<string, string>
{
{ Constants.ContentTypeHeader, Constants.TextPlainContentType }
};
await UploadLogFile(file, finalize, firstBlock, uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, cancellationToken, customHeaders);
// Update metadata // Update metadata
if (finalize) if (finalize)
@@ -411,7 +484,12 @@ namespace GitHub.Services.Results.Client
throw new Exception("Failed to get job log upload url"); throw new Exception("Failed to get job log upload url");
} }
await UploadLogFile(file, finalize, firstBlock, uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, cancellationToken); var customHeaders = new Dictionary<string, string>
{
{ Constants.ContentTypeHeader, Constants.TextPlainContentType }
};
await UploadLogFile(file, finalize, firstBlock, uploadUrlResponse.LogsUrl, uploadUrlResponse.BlobStorageType, cancellationToken, customHeaders);
// Update metadata // Update metadata
if (finalize) if (finalize)
@@ -421,6 +499,18 @@ namespace GitHub.Services.Results.Client
} }
} }
public async Task UploadResultsDiagnosticLogsAsync(string planId, string jobId, string file, CancellationToken cancellationToken)
{
// Get the upload url
var uploadUrlResponse = await GetDiagnosticLogsUploadUrlAsync(planId, jobId, cancellationToken);
if (uploadUrlResponse == null || uploadUrlResponse.DiagLogsURL == null)
{
throw new Exception("Failed to get diagnostic logs upload url");
}
await UploadLogFile(file, true, true, uploadUrlResponse.DiagLogsURL, uploadUrlResponse.BlobStorageType, cancellationToken);
}
private Step ConvertTimelineRecordToStep(TimelineRecord r) private Step ConvertTimelineRecordToStep(TimelineRecord r)
{ {
return new Step() return new Step()
@@ -511,6 +601,7 @@ namespace GitHub.Services.Results.Client
public static readonly string CreateStepLogsMetadata = ResultsReceiverTwirpEndpoint + "CreateStepLogsMetadata"; public static readonly string CreateStepLogsMetadata = ResultsReceiverTwirpEndpoint + "CreateStepLogsMetadata";
public static readonly string GetJobLogsSignedBlobURL = ResultsReceiverTwirpEndpoint + "GetJobLogsSignedBlobURL"; public static readonly string GetJobLogsSignedBlobURL = ResultsReceiverTwirpEndpoint + "GetJobLogsSignedBlobURL";
public static readonly string CreateJobLogsMetadata = ResultsReceiverTwirpEndpoint + "CreateJobLogsMetadata"; public static readonly string CreateJobLogsMetadata = ResultsReceiverTwirpEndpoint + "CreateJobLogsMetadata";
public static readonly string GetJobDiagLogsSignedBlobURL = ResultsReceiverTwirpEndpoint + "GetJobDiagLogsSignedBlobURL";
public static readonly string ResultsProtoApiV1Endpoint = "twirp/github.actions.results.api.v1.WorkflowStepUpdateService/"; public static readonly string ResultsProtoApiV1Endpoint = "twirp/github.actions.results.api.v1.WorkflowStepUpdateService/";
public static readonly string WorkflowStepsUpdate = ResultsProtoApiV1Endpoint + "WorkflowStepsUpdate"; public static readonly string WorkflowStepsUpdate = ResultsProtoApiV1Endpoint + "WorkflowStepsUpdate";
@@ -521,6 +612,9 @@ namespace GitHub.Services.Results.Client
public static readonly string AzureBlobTypeHeader = "x-ms-blob-type"; public static readonly string AzureBlobTypeHeader = "x-ms-blob-type";
public static readonly string AzureBlockBlob = "BlockBlob"; public static readonly string AzureBlockBlob = "BlockBlob";
public static readonly string AzureAppendBlob = "AppendBlob"; public static readonly string AzureAppendBlob = "AppendBlob";
public const string ContentTypeHeader = "Content-Type";
public const string TextPlainContentType = "text/plain";
} }
} }

View File

@@ -68,7 +68,7 @@ namespace GitHub.Runner.Common.Tests
trace.Info("Parsed"); trace.Info("Parsed");
trace.Info("Commands: {0}", clp.Commands.Count); trace.Info("Commands: {0}", clp.Commands.Count);
Assert.True(clp.Commands.Count == 2); Assert.Equal(2, clp.Commands.Count);
} }
} }
@@ -88,7 +88,7 @@ namespace GitHub.Runner.Common.Tests
trace.Info("Parsed"); trace.Info("Parsed");
trace.Info("Args: {0}", clp.Args.Count); trace.Info("Args: {0}", clp.Args.Count);
Assert.True(clp.Args.Count == 2); Assert.Equal(2, clp.Args.Count);
Assert.True(clp.Args.ContainsKey("arg1")); Assert.True(clp.Args.ContainsKey("arg1"));
Assert.Equal("arg1val", clp.Args["arg1"]); Assert.Equal("arg1val", clp.Args["arg1"]);
Assert.True(clp.Args.ContainsKey("arg2")); Assert.True(clp.Args.ContainsKey("arg2"));
@@ -112,7 +112,7 @@ namespace GitHub.Runner.Common.Tests
trace.Info("Parsed"); trace.Info("Parsed");
trace.Info("Args: {0}", clp.Flags.Count); trace.Info("Args: {0}", clp.Flags.Count);
Assert.True(clp.Flags.Count == 2); Assert.Equal(2, clp.Flags.Count);
Assert.Contains("flag1", clp.Flags); Assert.Contains("flag1", clp.Flags);
Assert.Contains("flag2", clp.Flags); Assert.Contains("flag2", clp.Flags);
} }

View File

@@ -24,7 +24,7 @@ namespace GitHub.Runner.Common.Tests
"osx-arm64" "osx-arm64"
}; };
Assert.True(BuildConstants.Source.CommitHash.Length == 40, $"CommitHash should be SHA-1 hash {BuildConstants.Source.CommitHash}"); Assert.Equal(40, BuildConstants.Source.CommitHash.Length);
Assert.True(validPackageNames.Contains(BuildConstants.RunnerPackage.PackageName), $"PackageName should be one of the following '{string.Join(", ", validPackageNames)}', current PackageName is '{BuildConstants.RunnerPackage.PackageName}'"); Assert.True(validPackageNames.Contains(BuildConstants.RunnerPackage.PackageName), $"PackageName should be one of the following '{string.Join(", ", validPackageNames)}', current PackageName is '{BuildConstants.RunnerPackage.PackageName}'");
} }
} }

View File

@@ -0,0 +1,81 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Listener;
using GitHub.Runner.Listener.Configuration;
using GitHub.Services.Common;
using Moq;
using Xunit;
namespace GitHub.Runner.Common.Tests.Listener
{
public sealed class BrokerMessageListenerL0
{
private readonly RunnerSettings _settings;
private readonly Mock<IConfigurationManager> _config;
private readonly Mock<IBrokerServer> _brokerServer;
private readonly Mock<ICredentialManager> _credMgr;
private Mock<IConfigurationStore> _store;
public BrokerMessageListenerL0()
{
_settings = new RunnerSettings { AgentId = 1, AgentName = "myagent", PoolId = 123, PoolName = "default", ServerUrl = "http://myserver", WorkFolder = "_work", ServerUrlV2 = "http://myserverv2" };
_config = new Mock<IConfigurationManager>();
_config.Setup(x => x.LoadSettings()).Returns(_settings);
_credMgr = new Mock<ICredentialManager>();
_store = new Mock<IConfigurationStore>();
_brokerServer = new Mock<IBrokerServer>();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void CreatesSession()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
var expectedSession = new TaskAgentSession();
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
// Act.
BrokerMessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result);
// Assert.
Assert.Equal(CreateSessionResult.Success, result);
_brokerServer
.Verify(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
}
}
private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
{
TestHostContext tc = new(this, testName);
tc.SetSingleton<IConfigurationManager>(_config.Object);
tc.SetSingleton<ICredentialManager>(_credMgr.Object);
tc.SetSingleton<IConfigurationStore>(_store.Object);
tc.SetSingleton<IBrokerServer>(_brokerServer.Object);
return tc;
}
}
}

View File

@@ -806,7 +806,7 @@ namespace GitHub.Runner.Common.Tests
"test runner" }); "test runner" });
// Assert. // Assert.
Assert.True(command.Validate().Count == 0); Assert.Equal(0, command.Validate().Count);
} }
} }
@@ -844,7 +844,7 @@ namespace GitHub.Runner.Common.Tests
var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{flag}" }); var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{flag}" });
// Assert. // Assert.
Assert.True(command.Validate().Count == 0); Assert.Equal(0, command.Validate().Count);
} }
} }
@@ -874,7 +874,7 @@ namespace GitHub.Runner.Common.Tests
var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{arg}", argValue }); var command = new CommandSettings(hc, args: new string[] { validCommand, $"--{arg}", argValue });
// Assert. // Assert.
Assert.True(command.Validate().Count == 0); Assert.Equal(0, command.Validate().Count);
} }
} }

View File

@@ -190,11 +190,11 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
trace.Info("Configured, verifying all the parameter value"); trace.Info("Configured, verifying all the parameter value");
var s = configManager.LoadSettings(); var s = configManager.LoadSettings();
Assert.NotNull(s); Assert.NotNull(s);
Assert.True(s.ServerUrl.Equals(_expectedServerUrl)); Assert.Equal(_expectedServerUrl, s.ServerUrl);
Assert.True(s.AgentName.Equals(_expectedAgentName)); Assert.Equal(_expectedAgentName, s.AgentName);
Assert.True(s.PoolId.Equals(_secondRunnerGroupId)); Assert.Equal(_secondRunnerGroupId, s.PoolId);
Assert.True(s.WorkFolder.Equals(_expectedWorkFolder)); Assert.Equal(_expectedWorkFolder, s.WorkFolder);
Assert.True(s.Ephemeral.Equals(true)); Assert.True(s.Ephemeral);
// validate GetAgentPoolsAsync gets called twice with automation pool type // validate GetAgentPoolsAsync gets called twice with automation pool type
_runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2)); _runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2));
@@ -292,11 +292,11 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
trace.Info("Configured, verifying all the parameter value"); trace.Info("Configured, verifying all the parameter value");
var s = configManager.LoadSettings(); var s = configManager.LoadSettings();
Assert.NotNull(s); Assert.NotNull(s);
Assert.True(s.ServerUrl.Equals(_expectedServerUrl)); Assert.Equal(_expectedServerUrl, s.ServerUrl);
Assert.True(s.AgentName.Equals(_expectedAgentName)); Assert.Equal(_expectedAgentName, s.AgentName);
Assert.True(s.PoolId.Equals(_secondRunnerGroupId)); Assert.Equal(_secondRunnerGroupId, s.PoolId);
Assert.True(s.WorkFolder.Equals(_expectedWorkFolder)); Assert.Equal(_expectedWorkFolder, s.WorkFolder);
Assert.True(s.Ephemeral.Equals(true)); Assert.True(s.Ephemeral);
// validate GetAgentPoolsAsync gets called twice with automation pool type // validate GetAgentPoolsAsync gets called twice with automation pool type
_runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2)); _runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2));

View File

@@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Listener;
using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Common.Tests;
using System.Runtime.CompilerServices;
using GitHub.Services.WebApi;
using Moq;
using Xunit;
namespace GitHub.Runner.Common.Tests.Listener
{
public sealed class ErrorThrottlerL0
{
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
[InlineData(6)]
[InlineData(7)]
[InlineData(8)]
public async void TestIncrementAndWait(int totalAttempts)
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange
var errorThrottler = new ErrorThrottler();
errorThrottler.Initialize(hc);
var eventArgs = new List<DelayEventArgs>();
hc.Delaying += (sender, args) =>
{
eventArgs.Add(args);
};
// Act
for (int attempt = 1; attempt <= totalAttempts; attempt++)
{
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
}
// Assert
Assert.Equal(totalAttempts - 1, eventArgs.Count);
for (int i = 0; i < eventArgs.Count; i++)
{
// Expected milliseconds
int expectedMin;
int expectedMax;
switch (i)
{
case 0:
expectedMin = 1000; // Min backoff
expectedMax = 1000;
break;
case 1:
expectedMin = 1800; // Min + 0.8 * Coefficient
expectedMax = 2200; // Min + 1.2 * Coefficient
break;
case 2:
expectedMin = 3400; // Min + 0.8 * Coefficient * 3
expectedMax = 4600; // Min + 1.2 * Coefficient * 3
break;
case 3:
expectedMin = 6600; // Min + 0.8 * Coefficient * 7
expectedMax = 9400; // Min + 1.2 * Coefficient * 7
break;
case 4:
expectedMin = 13000; // Min + 0.8 * Coefficient * 15
expectedMax = 19000; // Min + 1.2 * Coefficient * 15
break;
case 5:
expectedMin = 25800; // Min + 0.8 * Coefficient * 31
expectedMax = 38200; // Min + 1.2 * Coefficient * 31
break;
case 6:
expectedMin = 51400; // Min + 0.8 * Coefficient * 63
expectedMax = 60000; // Max backoff
break;
case 7:
expectedMin = 60000;
expectedMax = 60000;
break;
default:
throw new NotSupportedException("Unexpected eventArgs count");
}
var actualMilliseconds = eventArgs[i].Delay.TotalMilliseconds;
Assert.True(expectedMin <= actualMilliseconds, $"Unexpected min delay for eventArgs[{i}]. Expected min {expectedMin}, actual {actualMilliseconds}");
Assert.True(expectedMax >= actualMilliseconds, $"Unexpected max delay for eventArgs[{i}]. Expected max {expectedMax}, actual {actualMilliseconds}");
}
}
}
[Fact]
public async void TestReset()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange
var errorThrottler = new ErrorThrottler();
errorThrottler.Initialize(hc);
var eventArgs = new List<DelayEventArgs>();
hc.Delaying += (sender, args) =>
{
eventArgs.Add(args);
};
// Act
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
errorThrottler.Reset();
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
// Assert
Assert.Equal(4, eventArgs.Count);
for (int i = 0; i < eventArgs.Count; i++)
{
// Expected milliseconds
int expectedMin;
int expectedMax;
switch (i)
{
case 0:
case 2:
expectedMin = 1000; // Min backoff
expectedMax = 1000;
break;
case 1:
case 3:
expectedMin = 1800; // Min + 0.8 * Coefficient
expectedMax = 2200; // Min + 1.2 * Coefficient
break;
default:
throw new NotSupportedException("Unexpected eventArgs count");
}
var actualMilliseconds = eventArgs[i].Delay.TotalMilliseconds;
Assert.True(expectedMin <= actualMilliseconds, $"Unexpected min delay for eventArgs[{i}]. Expected min {expectedMin}, actual {actualMilliseconds}");
Assert.True(expectedMax >= actualMilliseconds, $"Unexpected max delay for eventArgs[{i}]. Expected max {expectedMax}, actual {actualMilliseconds}");
}
}
}
[Fact]
public async void TestReceivesCancellationToken()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange
var errorThrottler = new ErrorThrottler();
errorThrottler.Initialize(hc);
var eventArgs = new List<DelayEventArgs>();
hc.Delaying += (sender, args) =>
{
eventArgs.Add(args);
};
var cancellationTokenSource1 = new CancellationTokenSource();
var cancellationTokenSource2 = new CancellationTokenSource();
var cancellationTokenSource3 = new CancellationTokenSource();
// Act
await errorThrottler.IncrementAndWaitAsync(cancellationTokenSource1.Token);
await errorThrottler.IncrementAndWaitAsync(cancellationTokenSource2.Token);
await errorThrottler.IncrementAndWaitAsync(cancellationTokenSource3.Token);
// Assert
Assert.Equal(2, eventArgs.Count);
Assert.Equal(cancellationTokenSource2.Token, eventArgs[0].Token);
Assert.Equal(cancellationTokenSource3.Token, eventArgs[1].Token);
}
}
[Fact]
public async void TestReceivesSender()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange
var errorThrottler = new ErrorThrottler();
errorThrottler.Initialize(hc);
var senders = new List<object>();
hc.Delaying += (sender, args) =>
{
senders.Add(sender);
};
// Act
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
await errorThrottler.IncrementAndWaitAsync(CancellationToken.None);
// Assert
Assert.Equal(2, senders.Count);
Assert.Equal(hc, senders[0]);
Assert.Equal(hc, senders[1]);
}
}
private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
{
return new TestHostContext(this, testName);
}
}
}

View File

@@ -41,7 +41,7 @@ namespace GitHub.Runner.Common.Tests.Listener
TaskOrchestrationPlanReference plan = new(); TaskOrchestrationPlanReference plan = new();
TimelineReference timeline = null; TimelineReference timeline = null;
Guid jobId = Guid.NewGuid(); Guid jobId = Guid.NewGuid();
var result = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "someJob", "someJob", null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null); var result = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "someJob", "someJob", null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
result.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData(); result.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
return result; return result;
} }
@@ -734,7 +734,10 @@ namespace GitHub.Runner.Common.Tests.Listener
await jobDispatcher.WaitAsync(CancellationToken.None); await jobDispatcher.WaitAsync(CancellationToken.None);
Assert.True(jobDispatcher.RunOnceJobCompleted.Task.IsCompleted, "JobDispatcher should set task complete token for one time agent."); Assert.True(jobDispatcher.RunOnceJobCompleted.Task.IsCompleted, "JobDispatcher should set task complete token for one time agent.");
Assert.True(jobDispatcher.RunOnceJobCompleted.Task.Result, "JobDispatcher should set task complete token to 'TRUE' for one time agent."); if (jobDispatcher.RunOnceJobCompleted.Task.IsCompleted)
{
Assert.True(await jobDispatcher.RunOnceJobCompleted.Task, "JobDispatcher should set task complete token to 'TRUE' for one time agent.");
}
} }
} }
@@ -806,7 +809,8 @@ namespace GitHub.Runner.Common.Tests.Listener
}, },
null, null,
new List<TemplateToken>(), new List<TemplateToken>(),
new ActionsEnvironmentReference("env") new ActionsEnvironmentReference("env"),
null
); );
return message; return message;
} }

View File

@@ -24,6 +24,8 @@ namespace GitHub.Runner.Common.Tests.Listener
private Mock<ICredentialManager> _credMgr; private Mock<ICredentialManager> _credMgr;
private Mock<IConfigurationStore> _store; private Mock<IConfigurationStore> _store;
private Mock<IBrokerServer> _brokerServer;
public MessageListenerL0() public MessageListenerL0()
{ {
_settings = new RunnerSettings { AgentId = 1, AgentName = "myagent", PoolId = 123, PoolName = "default", ServerUrl = "http://myserver", WorkFolder = "_work" }; _settings = new RunnerSettings { AgentId = 1, AgentName = "myagent", PoolId = 123, PoolName = "default", ServerUrl = "http://myserver", WorkFolder = "_work" };
@@ -32,6 +34,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_runnerServer = new Mock<IRunnerServer>(); _runnerServer = new Mock<IRunnerServer>();
_credMgr = new Mock<ICredentialManager>(); _credMgr = new Mock<ICredentialManager>();
_store = new Mock<IConfigurationStore>(); _store = new Mock<IConfigurationStore>();
_brokerServer = new Mock<IBrokerServer>();
} }
private TestHostContext CreateTestContext([CallerMemberName] String testName = "") private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
@@ -41,6 +44,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tc.SetSingleton<IRunnerServer>(_runnerServer.Object); tc.SetSingleton<IRunnerServer>(_runnerServer.Object);
tc.SetSingleton<ICredentialManager>(_credMgr.Object); tc.SetSingleton<ICredentialManager>(_credMgr.Object);
tc.SetSingleton<IConfigurationStore>(_store.Object); tc.SetSingleton<IConfigurationStore>(_store.Object);
tc.SetSingleton<IBrokerServer>(_brokerServer.Object);
return tc; return tc;
} }
@@ -71,16 +75,82 @@ namespace GitHub.Runner.Common.Tests.Listener
MessageListener listener = new(); MessageListener listener = new();
listener.Initialize(tc); listener.Initialize(tc);
bool result = await listener.CreateSessionAsync(tokenSource.Token); CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result); trace.Info("result: {0}", result);
// Assert. // Assert.
Assert.True(result); Assert.Equal(CreateSessionResult.Success, result);
_runnerServer _runnerServer
.Verify(x => x.CreateAgentSessionAsync( .Verify(x => x.CreateAgentSessionAsync(
_settings.PoolId, _settings.PoolId,
It.Is<TaskAgentSession>(y => y != null), It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once()); tokenSource.Token), Times.Once());
_brokerServer
.Verify(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Never());
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void CreatesSessionWithBrokerMigration()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
var expectedSession = new TaskAgentSession()
{
OwnerName = "legacy",
BrokerMigrationMessage = new BrokerMigrationMessage(new Uri("https://broker.actions.github.com"))
};
var expectedBrokerSession = new TaskAgentSession()
{
OwnerName = "broker"
};
_runnerServer
.Setup(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedBrokerSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
// Act.
MessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result);
// Assert.
Assert.Equal(CreateSessionResult.Success, result);
_runnerServer
.Verify(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
_brokerServer
.Verify(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
} }
} }
@@ -115,8 +185,8 @@ namespace GitHub.Runner.Common.Tests.Listener
MessageListener listener = new(); MessageListener listener = new();
listener.Initialize(tc); listener.Initialize(tc);
bool result = await listener.CreateSessionAsync(tokenSource.Token); CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
Assert.True(result); Assert.Equal(CreateSessionResult.Success, result);
_runnerServer _runnerServer
.Setup(x => x.DeleteAgentSessionAsync( .Setup(x => x.DeleteAgentSessionAsync(
@@ -131,6 +201,83 @@ namespace GitHub.Runner.Common.Tests.Listener
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void DeleteSessionWithBrokerMigration()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
var expectedSession = new TaskAgentSession()
{
OwnerName = "legacy",
BrokerMigrationMessage = new BrokerMigrationMessage(new Uri("https://broker.actions.github.com"))
};
var expectedBrokerSession = new TaskAgentSession()
{
SessionId = Guid.NewGuid(),
OwnerName = "broker"
};
_runnerServer
.Setup(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedBrokerSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
// Act.
MessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result);
Assert.Equal(CreateSessionResult.Success, result);
_runnerServer
.Verify(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
_brokerServer
.Verify(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
_brokerServer
.Setup(x => x.DeleteSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
// Act.
await listener.DeleteSessionAsync();
//Assert
_runnerServer
.Verify(x => x.DeleteAgentSessionAsync(
_settings.PoolId, expectedBrokerSession.SessionId, It.IsAny<CancellationToken>()), Times.Once());
_brokerServer
.Verify(x => x.DeleteSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
}
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
@@ -162,8 +309,8 @@ namespace GitHub.Runner.Common.Tests.Listener
MessageListener listener = new(); MessageListener listener = new();
listener.Initialize(tc); listener.Initialize(tc);
bool result = await listener.CreateSessionAsync(tokenSource.Token); CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
Assert.True(result); Assert.Equal(CreateSessionResult.Success, result);
var arMessages = new TaskAgentMessage[] var arMessages = new TaskAgentMessage[]
{ {
@@ -212,6 +359,112 @@ namespace GitHub.Runner.Common.Tests.Listener
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void GetNextMessageWithBrokerMigration()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
var expectedSession = new TaskAgentSession();
PropertyInfo sessionIdProperty = expectedSession.GetType().GetProperty("SessionId", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
Assert.NotNull(sessionIdProperty);
sessionIdProperty.SetValue(expectedSession, Guid.NewGuid());
_runnerServer
.Setup(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
// Act.
MessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
Assert.Equal(CreateSessionResult.Success, result);
var brokerMigrationMesage = new BrokerMigrationMessage(new Uri("https://actions.broker.com"));
var arMessages = new TaskAgentMessage[]
{
new TaskAgentMessage
{
Body = JsonUtility.ToString(brokerMigrationMesage),
MessageType = BrokerMigrationMessage.MessageType
},
};
var brokerMessages = new TaskAgentMessage[]
{
new TaskAgentMessage
{
Body = "somebody1",
MessageId = 4234,
MessageType = JobRequestMessageTypes.PipelineAgentJobRequest
},
new TaskAgentMessage
{
Body = "somebody2",
MessageId = 4235,
MessageType = JobCancelMessage.MessageType
},
null, //should be skipped by GetNextMessageAsync implementation
null,
new TaskAgentMessage
{
Body = "somebody3",
MessageId = 4236,
MessageType = JobRequestMessageTypes.PipelineAgentJobRequest
}
};
var brokerMessageQueue = new Queue<TaskAgentMessage>(brokerMessages);
_runnerServer
.Setup(x => x.GetAgentMessageAsync(
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(async (Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, string runnerVersion, string os, string architecture, bool disableUpdate, CancellationToken cancellationToken) =>
{
await Task.Yield();
return arMessages[0]; // always send migration message
});
_brokerServer
.Setup(x => x.GetRunnerMessageAsync(
expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(async (Guid sessionId, TaskAgentStatus status, string runnerVersion, string os, string architecture, bool disableUpdate, CancellationToken cancellationToken) =>
{
await Task.Yield();
return brokerMessageQueue.Dequeue();
});
TaskAgentMessage message1 = await listener.GetNextMessageAsync(tokenSource.Token);
TaskAgentMessage message2 = await listener.GetNextMessageAsync(tokenSource.Token);
TaskAgentMessage message3 = await listener.GetNextMessageAsync(tokenSource.Token);
Assert.Equal(brokerMessages[0], message1);
Assert.Equal(brokerMessages[1], message2);
Assert.Equal(brokerMessages[4], message3);
//Assert
_runnerServer
.Verify(x => x.GetAgentMessageAsync(
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length));
_brokerServer
.Verify(x => x.GetRunnerMessageAsync(
expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length));
}
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
@@ -244,11 +497,11 @@ namespace GitHub.Runner.Common.Tests.Listener
MessageListener listener = new(); MessageListener listener = new();
listener.Initialize(tc); listener.Initialize(tc);
bool result = await listener.CreateSessionAsync(tokenSource.Token); CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result); trace.Info("result: {0}", result);
// Assert. // Assert.
Assert.True(result); Assert.Equal(CreateSessionResult.Success, result);
_runnerServer _runnerServer
.Verify(x => x.CreateAgentSessionAsync( .Verify(x => x.CreateAgentSessionAsync(
_settings.PoolId, _settings.PoolId,
@@ -288,8 +541,8 @@ namespace GitHub.Runner.Common.Tests.Listener
MessageListener listener = new(); MessageListener listener = new();
listener.Initialize(tc); listener.Initialize(tc);
bool result = await listener.CreateSessionAsync(tokenSource.Token); CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
Assert.True(result); Assert.Equal(CreateSessionResult.Success, result);
_runnerServer _runnerServer
.Setup(x => x.GetAgentMessageAsync( .Setup(x => x.GetAgentMessageAsync(

View File

@@ -23,6 +23,7 @@ namespace GitHub.Runner.Common.Tests.Listener
private Mock<ITerminal> _term; private Mock<ITerminal> _term;
private Mock<IConfigurationStore> _configStore; private Mock<IConfigurationStore> _configStore;
private Mock<ISelfUpdater> _updater; private Mock<ISelfUpdater> _updater;
private Mock<IErrorThrottler> _acquireJobThrottler;
public RunnerL0() public RunnerL0()
{ {
@@ -35,6 +36,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_term = new Mock<ITerminal>(); _term = new Mock<ITerminal>();
_configStore = new Mock<IConfigurationStore>(); _configStore = new Mock<IConfigurationStore>();
_updater = new Mock<ISelfUpdater>(); _updater = new Mock<ISelfUpdater>();
_acquireJobThrottler = new Mock<IErrorThrottler>();
} }
private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName) private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName)
@@ -42,7 +44,7 @@ namespace GitHub.Runner.Common.Tests.Listener
TaskOrchestrationPlanReference plan = new(); TaskOrchestrationPlanReference plan = new();
TimelineReference timeline = null; TimelineReference timeline = null;
Guid jobId = Guid.NewGuid(); Guid jobId = Guid.NewGuid();
return new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null); return new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
} }
private JobCancelMessage CreateJobCancelMessage() private JobCancelMessage CreateJobCancelMessage()
@@ -67,6 +69,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IPromptManager>(_promptManager.Object); hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object); hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object); hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
runner.Initialize(hc); runner.Initialize(hc);
var settings = new RunnerSettings var settings = new RunnerSettings
{ {
@@ -88,7 +91,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_configurationManager.Setup(x => x.IsConfigured()) _configurationManager.Setup(x => x.IsConfigured())
.Returns(true); .Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<bool>(true)); .Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
.Returns(async () => .Returns(async () =>
{ {
@@ -126,7 +129,7 @@ namespace GitHub.Runner.Common.Tests.Listener
//wait for the runner to run one job //wait for the runner to run one job
if (!await signalWorkerComplete.WaitAsync(2000)) if (!await signalWorkerComplete.WaitAsync(2000))
{ {
Assert.True(false, $"{nameof(_messageListener.Object.GetNextMessageAsync)} was not invoked."); Assert.Fail($"{nameof(_messageListener.Object.GetNextMessageAsync)} was not invoked.");
} }
else else
{ {
@@ -174,6 +177,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IPromptManager>(_promptManager.Object); hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object); hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object); hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
var command = new CommandSettings(hc, args); var command = new CommandSettings(hc, args);
@@ -184,7 +188,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(configureAsService); _configStore.Setup(x => x.IsServiceConfigured()).Returns(configureAsService);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(false)); .Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Failure));
var runner = new Runner.Listener.Runner(); var runner = new Runner.Listener.Runner();
runner.Initialize(hc); runner.Initialize(hc);
@@ -205,6 +209,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IPromptManager>(_promptManager.Object); hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object); hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object); hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
var command = new CommandSettings(hc, new[] { "run" }); var command = new CommandSettings(hc, new[] { "run" });
@@ -217,7 +222,7 @@ namespace GitHub.Runner.Common.Tests.Listener
.Returns(false); .Returns(false);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(false)); .Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Failure));
var runner = new Runner.Listener.Runner(); var runner = new Runner.Listener.Runner();
runner.Initialize(hc); runner.Initialize(hc);
@@ -242,6 +247,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IPromptManager>(_promptManager.Object); hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object); hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object); hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
runner.Initialize(hc); runner.Initialize(hc);
var settings = new RunnerSettings var settings = new RunnerSettings
{ {
@@ -263,7 +269,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_configurationManager.Setup(x => x.IsConfigured()) _configurationManager.Setup(x => x.IsConfigured())
.Returns(true); .Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<bool>(true)); .Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
.Returns(async () => .Returns(async () =>
{ {
@@ -305,8 +311,11 @@ namespace GitHub.Runner.Common.Tests.Listener
await Task.WhenAny(runnerTask, Task.Delay(30000)); await Task.WhenAny(runnerTask, Task.Delay(30000));
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out."); Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString()); Assert.False(runnerTask.IsFaulted, runnerTask.Exception?.ToString());
Assert.True(runnerTask.Result == Constants.Runner.ReturnCode.Success); if (runnerTask.IsCompleted)
{
Assert.Equal(Constants.Runner.ReturnCode.Success, await runnerTask);
}
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once(), _jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once(),
$"{nameof(_jobDispatcher.Object.Run)} was not invoked."); $"{nameof(_jobDispatcher.Object.Run)} was not invoked.");
@@ -335,6 +344,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IPromptManager>(_promptManager.Object); hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object); hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object); hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
runner.Initialize(hc); runner.Initialize(hc);
var settings = new RunnerSettings var settings = new RunnerSettings
{ {
@@ -363,7 +373,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_configurationManager.Setup(x => x.IsConfigured()) _configurationManager.Setup(x => x.IsConfigured())
.Returns(true); .Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<bool>(true)); .Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
.Returns(async () => .Returns(async () =>
{ {
@@ -406,7 +416,10 @@ namespace GitHub.Runner.Common.Tests.Listener
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out."); Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString()); Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString());
Assert.True(runnerTask.Result == Constants.Runner.ReturnCode.Success); if (runnerTask.IsCompleted)
{
Assert.Equal(Constants.Runner.ReturnCode.Success, await runnerTask);
}
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once(), _jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once(),
$"{nameof(_jobDispatcher.Object.Run)} was not invoked."); $"{nameof(_jobDispatcher.Object.Run)} was not invoked.");
@@ -433,6 +446,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IRunnerServer>(_runnerServer.Object); hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object); hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<ISelfUpdater>(_updater.Object); hc.SetSingleton<ISelfUpdater>(_updater.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
runner.Initialize(hc); runner.Initialize(hc);
var settings = new RunnerSettings var settings = new RunnerSettings
@@ -458,7 +472,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_configurationManager.Setup(x => x.IsConfigured()) _configurationManager.Setup(x => x.IsConfigured())
.Returns(true); .Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<bool>(true)); .Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>())) _messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
.Returns(async () => .Returns(async () =>
{ {
@@ -492,7 +506,10 @@ namespace GitHub.Runner.Common.Tests.Listener
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out."); Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString()); Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString());
Assert.True(runnerTask.Result == Constants.Runner.ReturnCode.RunOnceRunnerUpdating); if (runnerTask.IsCompleted)
{
Assert.Equal(Constants.Runner.ReturnCode.RunOnceRunnerUpdating, await runnerTask);
}
_updater.Verify(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), false, It.IsAny<CancellationToken>()), Times.Once); _updater.Verify(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), false, It.IsAny<CancellationToken>()), Times.Once);
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Never()); _jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Never());
@@ -513,6 +530,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object); hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object); hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<IPromptManager>(_promptManager.Object); hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
var command = new CommandSettings(hc, new[] { "remove", "--local" }); var command = new CommandSettings(hc, new[] { "remove", "--local" });

View File

@@ -58,7 +58,7 @@ namespace GitHub.Runner.Common.Tests
trace.Error(ex); trace.Error(ex);
} }
Assert.True(false, "Fail to retrive process environment variable."); Assert.Fail("Failed to retrieve process environment variable.");
} }
finally finally
{ {

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