Compare commits

...

30 Commits

Author SHA1 Message Date
Tingluo Huang
da3412efd4 Create 2.324.0 runner release. 2025-05-12 21:49:26 -04:00
Tingluo Huang
26185d43d0 Prepare 2.324.0 runner release. (#3856) 2025-05-12 19:29:05 -04:00
Tingluo Huang
e911d2908d Update docker and buildx (#3854) 2025-05-12 17:54:44 -04:00
Lokesh Gopu
ce4b7f4dd6 Prefer _migrated config on startup (#3853) 2025-05-12 16:54:43 -04:00
Tingluo Huang
505fa60905 Make sure the token's claims are match as expected. (#3846)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 17:35:38 -04:00
dependabot[bot]
57459ad274 Bump xunit.runner.visualstudio from 2.5.8 to 2.8.2 in /src (#3845)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-07 10:38:40 -04:00
dependabot[bot]
890e43f6c5 Bump System.ServiceProcess.ServiceController from 8.0.0 to 8.0.1 in /src (#3844)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-07 10:27:05 -04:00
Patrick Ellis
3a27ca292a Feature-flagged support for JobContext.CheckRunId (#3811) 2025-05-06 11:45:51 -04:00
Tingluo Huang
282f7cd2b2 Bump nodejs version. (#3840) 2025-05-06 10:42:55 -04:00
dependabot[bot]
f060fe5c85 Bump Azure.Storage.Blobs from 12.23.0 to 12.24.0 in /src (#3837)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 08:14:07 -04:00
David Sanders
1a092a24a3 feat: default fromPath for problem matchers (#3802) 2025-05-05 20:45:23 +00:00
Tingluo Huang
26eff8e55a Ignore exception during auth migration. (#3835) 2025-05-05 14:23:24 -04:00
dependabot[bot]
d7cfd2e341 Bump actions/upload-release-asset from 1.0.1 to 1.0.2 (#3553)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 04:24:49 +00:00
Patrick Ellis
a3a7b6a77e Add copilot-instructions.md (#3810) 2025-05-05 00:17:20 -04:00
dependabot[bot]
db6005b0a7 Bump Microsoft.NET.Test.Sdk from 17.12.0 to 17.13.0 in /src (#3719)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 04:15:50 +00:00
eric sciple
9155c42c09 Do not retry /renewjob on 404 (#3828) 2025-04-30 14:44:40 -05:00
Tingluo Huang
1c319b4d42 Allow enable auth migration by default. (#3804) 2025-04-23 16:57:54 -04:00
Nikola Jokic
fe10d4ae82 Bump hook to 0.7.0 (#3813) 2025-04-17 09:32:34 -04:00
github-actions[bot]
27d9c886ab Update dotnet sdk to latest version @8.0.408 (#3808)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-14 10:32:45 -04:00
eric sciple
5106d6578e Cleanup feature flag actions_skip_retry_complete_job_upon_known_errors (#3806) 2025-04-11 08:34:17 -05:00
Tingluo Huang
d5ccbd10d1 Support auth migration using authUrlV2 in Runner/MessageListener. (#3787) 2025-04-10 12:58:33 -04:00
Tingluo Huang
f1b5b5bd5c Enable FIPS by default. (#3793) 2025-04-07 15:53:53 +00:00
Tingluo Huang
aaf1b92847 Set JWT.alg to PS256 with PssPadding. (#3789) 2025-04-07 11:49:14 -04:00
Tingluo Huang
c1095ae2d1 Enable auth migration based on config refresh. (#3786) 2025-04-02 23:24:57 -04:00
Tingluo Huang
a0a0a76378 Remove create session with broker in MessageListener. (#3782) 2025-04-01 12:24:01 -04:00
Tingluo Huang
d47013928b Add option in OAuthCred to load authUrlV2. (#3777) 2025-03-31 17:05:41 -04:00
Tingluo Huang
cdeec012aa Enable hostcontext to track auth migration. (#3776) 2025-03-31 15:26:56 -04:00
Tingluo Huang
2cb1f9431a Small runner code cleanup. (#3773) 2025-03-28 16:25:12 -04:00
Tingluo Huang
e86c9487ab Fix release.yml break by upgrading actions/github-script (#3772) 2025-03-28 12:20:15 -04:00
eric sciple
dc9695f123 Increase error body max length before truncation (#3762) 2025-03-20 20:09:00 -05:00
48 changed files with 2804 additions and 380 deletions

View File

@@ -4,7 +4,7 @@
"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": "8.0.407" "version": "8.0.408"
}, },
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "20" "version": "20"

25
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,25 @@
## Making changes
### Tests
Whenever possible, changes should be accompanied by non-trivial tests that meaningfully exercise the core functionality of the new code being introduced.
All tests are in the `Test/` directory at the repo root. Fast unit tests are in the `Test/L0` directory and by convention have the suffix `L0.cs`. For example: unit tests for a hypothetical `src/Runner.Worker/Foo.cs` would go in `src/Test/L0/Worker/FooL0.cs`.
Run tests using this command:
```sh
cd src && ./dev.sh test
```
### Formatting
After editing .cs files, always format the code using this command:
```sh
cd src && ./dev.sh format
```
### Feature Flags
Wherever possible, all changes should be safeguarded by a feature flag; `Features` are declared in [Constants.cs](src/Runner.Common/Constants.cs).

View File

@@ -20,7 +20,6 @@ jobs:
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
const core = require('@actions/core')
const fs = require('fs'); const fs = require('fs');
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '') const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
const releaseVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '') const releaseVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '')
@@ -30,7 +29,7 @@ jobs:
return return
} }
try { try {
const release = await github.repos.getReleaseByTag({ const release = await github.rest.repos.getReleaseByTag({
owner: '${{ github.event.repository.owner.name }}', owner: '${{ github.event.repository.owner.name }}',
repo: '${{ github.event.repository.name }}', repo: '${{ github.event.repository.name }}',
tag: 'v' + runnerVersion tag: 'v' + runnerVersion
@@ -176,7 +175,6 @@ jobs:
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
const core = require('@actions/core')
const fs = require('fs'); const fs = require('fs');
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '') const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
var releaseNote = fs.readFileSync('${{ github.workspace }}/releaseNote.md', 'utf8').replace(/<RUNNER_VERSION>/g, runnerVersion) var releaseNote = fs.readFileSync('${{ github.workspace }}/releaseNote.md', 'utf8').replace(/<RUNNER_VERSION>/g, runnerVersion)
@@ -216,7 +214,7 @@ jobs:
# Upload release assets (full runner packages) # Upload release assets (full runner packages)
- name: Upload Release Asset (win-x64) - name: Upload Release Asset (win-x64)
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -226,7 +224,7 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload Release Asset (win-arm64) - name: Upload Release Asset (win-arm64)
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -236,7 +234,7 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload Release Asset (linux-x64) - name: Upload Release Asset (linux-x64)
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -246,7 +244,7 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload Release Asset (osx-x64) - name: Upload Release Asset (osx-x64)
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -256,7 +254,7 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload Release Asset (osx-arm64) - name: Upload Release Asset (osx-arm64)
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -266,7 +264,7 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload Release Asset (linux-arm) - name: Upload Release Asset (linux-arm)
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -276,7 +274,7 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload Release Asset (linux-arm64) - name: Upload Release Asset (linux-arm64)
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:

View File

@@ -250,6 +250,42 @@ Two problem matchers can be used:
} }
``` ```
#### Default from path
The problem matcher can specify a `fromPath` property at the top level, which applies when a specific pattern doesn't provide a value for `fromPath`. This is useful for tools that don't include project file information in their output.
For example, given the following compiler output that doesn't include project file information:
```
ClassLibrary.cs(16,24): warning CS0612: 'ClassLibrary.Helpers.MyHelper.Name' is obsolete
```
A problem matcher with a default from path can be used:
```json
{
"problemMatcher": [
{
"owner": "csc-minimal",
"fromPath": "ClassLibrary/ClassLibrary.csproj",
"pattern": [
{
"regexp": "^(.+)\\((\\d+),(\\d+)\\): (error|warning) (.+): (.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"code": 5,
"message": 6
}
]
}
]
}
```
This ensures that the file is rooted to the correct path when there's not enough information in the error messages to extract a `fromPath`.
#### Mitigate regular expression denial of service (ReDos) #### Mitigate regular expression denial of service (ReDos)
If a matcher exceeds a 1 second timeout when processing a line, retry up to two three times total. If a matcher exceeds a 1 second timeout when processing a line, retry up to two three times total.

View File

@@ -4,9 +4,9 @@ FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy AS build
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
ARG RUNNER_VERSION ARG RUNNER_VERSION
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.6.1 ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
ARG DOCKER_VERSION=28.0.1 ARG DOCKER_VERSION=28.1.1
ARG BUILDX_VERSION=0.21.2 ARG BUILDX_VERSION=0.23.0
RUN apt update -y && apt install curl unzip -y RUN apt update -y && apt install curl unzip -y

View File

@@ -1,36 +1,37 @@
## What's Changed ## What's Changed
* Bump docker/login-action from 2 to 3 by @dependabot in https://github.com/actions/runner/pull/3673 * Increase error body max length before truncation by @ericsciple in https://github.com/actions/runner/pull/3762
* Bump actions/stale from 8 to 9 by @dependabot in https://github.com/actions/runner/pull/3554 * Fix release.yml break by upgrading actions/github-script by @TingluoHuang in https://github.com/actions/runner/pull/3772
* Bump docker/build-push-action from 3 to 6 by @dependabot in https://github.com/actions/runner/pull/3674 * Small runner code cleanup. by @TingluoHuang in https://github.com/actions/runner/pull/3773
* update node version from 20.18.0 -> 20.18.2 by @aiqiaoy in https://github.com/actions/runner/pull/3682 * Enable hostcontext to track auth migration. by @TingluoHuang in https://github.com/actions/runner/pull/3776
* Pass BillingOwnerId through Acquire/Complete calls by @luketomlinson in https://github.com/actions/runner/pull/3689 * Add option in OAuthCred to load authUrlV2. by @TingluoHuang in https://github.com/actions/runner/pull/3777
* Do not retry CompleteJobAsync for known non-retryable errors by @ericsciple in https://github.com/actions/runner/pull/3696 * Remove create session with broker in MessageListener. by @TingluoHuang in https://github.com/actions/runner/pull/3782
* Update dotnet sdk to latest version @8.0.406 by @github-actions in https://github.com/actions/runner/pull/3712 * Enable auth migration based on config refresh. by @TingluoHuang in https://github.com/actions/runner/pull/3786
* Update Dockerfile with new docker and buildx versions by @thboop in https://github.com/actions/runner/pull/3680 * Set JWT.alg to PS256 with PssPadding. by @TingluoHuang in https://github.com/actions/runner/pull/3789
* chore: remove redundant words by @finaltrip in https://github.com/actions/runner/pull/3705 * Enable FIPS by default. by @TingluoHuang in https://github.com/actions/runner/pull/3793
* fix: actions feedback link is incorrect by @Yaminyam in https://github.com/actions/runner/pull/3165 * Support auth migration using authUrlV2 in Runner/MessageListener. by @TingluoHuang in https://github.com/actions/runner/pull/3787
* Bump actions/github-script from 0.3.0 to 7.0.1 by @dependabot in https://github.com/actions/runner/pull/3557 * Cleanup feature flag actions_skip_retry_complete_job_upon_known_errors by @ericsciple in https://github.com/actions/runner/pull/3806
* Docker container provenance by @paveliak in https://github.com/actions/runner/pull/3736 * Update dotnet sdk to latest version @8.0.408 by @github-actions in https://github.com/actions/runner/pull/3808
* Add request-id to http eventsource trace. by @TingluoHuang in https://github.com/actions/runner/pull/3740 * Bump hook to 0.7.0 by @nikola-jokic in https://github.com/actions/runner/pull/3813
* Update Bocker and Buildx version to mitigate images scanners alerts by @Blizter in https://github.com/actions/runner/pull/3750 * Allow enable auth migration by default. by @TingluoHuang in https://github.com/actions/runner/pull/3804
* Fix typo, add invariant culture to timestamp for workflow log reporting by @GhadimiR in https://github.com/actions/runner/pull/3749 * Do not retry /renewjob on 404 by @ericsciple in https://github.com/actions/runner/pull/3828
* Create vssconnection to actions service when URL provided. by @TingluoHuang in https://github.com/actions/runner/pull/3751 * Bump Microsoft.NET.Test.Sdk from 17.12.0 to 17.13.0 in /src by @dependabot in https://github.com/actions/runner/pull/3719
* Housekeeping: Update npm packages and node version by @thboop in https://github.com/actions/runner/pull/3752 * Add copilot-instructions.md by @pje in https://github.com/actions/runner/pull/3810
* Improve the out-of-date warning message. by @tecimovic in https://github.com/actions/runner/pull/3595 * Bump actions/upload-release-asset from 1.0.1 to 1.0.2 by @dependabot in https://github.com/actions/runner/pull/3553
* Update dotnet sdk to latest version @8.0.407 by @github-actions in https://github.com/actions/runner/pull/3753 * Ignore exception during auth migration. by @TingluoHuang in https://github.com/actions/runner/pull/3835
* Exit hosted runner cleanly during deprovisioning. by @TingluoHuang in https://github.com/actions/runner/pull/3755 * feat: default fromPath for problem matchers by @dsanders11 in https://github.com/actions/runner/pull/3802
* Send annotation title to run-service. by @TingluoHuang in https://github.com/actions/runner/pull/3757 * Bump Azure.Storage.Blobs from 12.23.0 to 12.24.0 in /src by @dependabot in https://github.com/actions/runner/pull/3837
* Allow server enforce runner settings. by @TingluoHuang in https://github.com/actions/runner/pull/3758 * Bump nodejs version. by @TingluoHuang in https://github.com/actions/runner/pull/3840
* Support refresh runner configs with pipelines service. by @TingluoHuang in https://github.com/actions/runner/pull/3706 * Feature-flagged support for `JobContext.CheckRunID` by @pje in https://github.com/actions/runner/pull/3811
* Bump System.ServiceProcess.ServiceController from 8.0.0 to 8.0.1 in /src by @dependabot in https://github.com/actions/runner/pull/3844
* Bump xunit.runner.visualstudio from 2.5.8 to 2.8.2 in /src by @dependabot in https://github.com/actions/runner/pull/3845
* Make sure the token's claims are match as expected. by @TingluoHuang in https://github.com/actions/runner/pull/3846
* Prefer _migrated config on startup by @lokesh755 in https://github.com/actions/runner/pull/3853
* Update docker and buildx by @TingluoHuang in https://github.com/actions/runner/pull/3854
## New Contributors ## New Contributors
* @finaltrip made their first contribution in https://github.com/actions/runner/pull/3705 * @dsanders11 made their first contribution in https://github.com/actions/runner/pull/3802
* @Yaminyam made their first contribution in https://github.com/actions/runner/pull/3165
* @Blizter made their first contribution in https://github.com/actions/runner/pull/3750
* @GhadimiR made their first contribution in https://github.com/actions/runner/pull/3749
* @tecimovic made their first contribution in https://github.com/actions/runner/pull/3595
**Full Changelog**: https://github.com/actions/runner/compare/v2.322.0...v2.323.0 **Full Changelog**: https://github.com/actions/runner/compare/v2.323.0...v2.324.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.

View File

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

View File

@@ -6,7 +6,7 @@ NODE_URL=https://nodejs.org/dist
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. # 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 # Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
NODE20_VERSION="20.19.0" NODE20_VERSION="20.19.1"
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

@@ -0,0 +1,13 @@
using System;
namespace GitHub.Runner.Common
{
public class AuthMigrationEventArgs : EventArgs
{
public AuthMigrationEventArgs(string trace)
{
Trace = trace;
}
public string Trace { get; private set; }
}
}

View File

@@ -37,6 +37,7 @@ namespace GitHub.Runner.Common
public async Task ConnectAsync(Uri serverUri, VssCredentials credentials) public async Task ConnectAsync(Uri serverUri, VssCredentials credentials)
{ {
Trace.Entering();
_brokerUri = serverUri; _brokerUri = serverUri;
_connection = VssUtil.CreateRawConnection(serverUri, credentials); _connection = VssUtil.CreateRawConnection(serverUri, credentials);
@@ -88,7 +89,12 @@ namespace GitHub.Runner.Common
public Task ForceRefreshConnection(VssCredentials credentials) public Task ForceRefreshConnection(VssCredentials credentials)
{ {
return ConnectAsync(_brokerUri, credentials); if (!string.IsNullOrEmpty(_brokerUri?.AbsoluteUri))
{
return ConnectAsync(_brokerUri, credentials);
}
return Task.CompletedTask;
} }
public bool ShouldRetryException(Exception ex) public bool ShouldRetryException(Exception ex)

View File

@@ -116,6 +116,7 @@ namespace GitHub.Runner.Common
bool IsConfigured(); bool IsConfigured();
bool IsServiceConfigured(); bool IsServiceConfigured();
bool HasCredentials(); bool HasCredentials();
bool IsMigratedConfigured();
CredentialData GetCredentials(); CredentialData GetCredentials();
CredentialData GetMigratedCredentials(); CredentialData GetMigratedCredentials();
RunnerSettings GetSettings(); RunnerSettings GetSettings();
@@ -198,6 +199,14 @@ namespace GitHub.Runner.Common
return serviceConfigured; return serviceConfigured;
} }
public bool IsMigratedConfigured()
{
Trace.Info("IsMigratedConfigured()");
bool configured = new FileInfo(_migratedConfigFilePath).Exists;
Trace.Info("IsMigratedConfigured: {0}", configured);
return configured;
}
public CredentialData GetCredentials() public CredentialData GetCredentials()
{ {
if (_creds == null) if (_creds == null)

View File

@@ -155,15 +155,19 @@ namespace GitHub.Runner.Common
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 const int SessionConflict = 5;
// Temporary error code to indicate that the runner configuration has been refreshed
// and the runner should be restarted. This is a temporary code and will be removed in the future after
// the runner is migrated to runner admin.
public const int RunnerConfigurationRefreshed = 6;
} }
public static class Features public static class Features
{ {
public static readonly string DiskSpaceWarning = "runner.diskspace.warning"; public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
public static readonly string LogTemplateErrorsAsDebugMessages = "DistributedTask.LogTemplateErrorsAsDebugMessages"; public static readonly string LogTemplateErrorsAsDebugMessages = "DistributedTask.LogTemplateErrorsAsDebugMessages";
public static readonly string SkipRetryCompleteJobUponKnownErrors = "actions_skip_retry_complete_job_upon_known_errors";
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate"; public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks"; public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
} }
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry"; public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";

View File

@@ -37,6 +37,11 @@ namespace GitHub.Runner.Common
void ShutdownRunner(ShutdownReason reason); void ShutdownRunner(ShutdownReason reason);
void WritePerfCounter(string counter); void WritePerfCounter(string counter);
void LoadDefaultUserAgents(); void LoadDefaultUserAgents();
bool AllowAuthMigration { get; }
void EnableAuthMigration(string trace);
void DeferAuthMigration(TimeSpan deferred, string trace);
event EventHandler<AuthMigrationEventArgs> AuthMigrationChanged;
} }
public enum StartupType public enum StartupType
@@ -70,12 +75,21 @@ namespace GitHub.Runner.Common
private RunnerWebProxy _webProxy = new(); private RunnerWebProxy _webProxy = new();
private string _hostType = string.Empty; private string _hostType = string.Empty;
// disable auth migration by default
private readonly ManualResetEventSlim _allowAuthMigration = new ManualResetEventSlim(false);
private DateTime _deferredAuthMigrationTime = DateTime.MaxValue;
private readonly object _authMigrationLock = new object();
private CancellationTokenSource _authMigrationAutoReenableTaskCancellationTokenSource = new();
private Task _authMigrationAutoReenableTask;
public event EventHandler Unloading; public event EventHandler Unloading;
public event EventHandler<AuthMigrationEventArgs> AuthMigrationChanged;
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token; public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
public ShutdownReason RunnerShutdownReason { get; private set; } public ShutdownReason RunnerShutdownReason { get; private set; }
public ISecretMasker SecretMasker => _secretMasker; public ISecretMasker SecretMasker => _secretMasker;
public List<ProductInfoHeaderValue> UserAgents => _userAgents; public List<ProductInfoHeaderValue> UserAgents => _userAgents;
public RunnerWebProxy WebProxy => _webProxy; public RunnerWebProxy WebProxy => _webProxy;
public bool AllowAuthMigration => _allowAuthMigration.IsSet;
public HostContext(string hostType, string logFile = null) public HostContext(string hostType, string logFile = null)
{ {
// Validate args. // Validate args.
@@ -207,6 +221,71 @@ namespace GitHub.Runner.Common
LoadDefaultUserAgents(); LoadDefaultUserAgents();
} }
// marked as internal for testing
internal async Task AuthMigrationAuthReenableAsync(TimeSpan refreshInterval, CancellationToken token)
{
try
{
while (!token.IsCancellationRequested)
{
_trace.Verbose($"Auth migration defer timer is set to expire at {_deferredAuthMigrationTime.ToString("O")}. AllowAuthMigration: {_allowAuthMigration.IsSet}.");
await Task.Delay(refreshInterval, token);
if (!_allowAuthMigration.IsSet && DateTime.UtcNow > _deferredAuthMigrationTime)
{
_trace.Info($"Auth migration defer timer expired. Allowing auth migration.");
EnableAuthMigration("Auth migration defer timer expired.");
}
}
}
catch (TaskCanceledException)
{
// Task was cancelled, exit the loop.
}
catch (Exception ex)
{
_trace.Info("Error in auth migration reenable task.");
_trace.Error(ex);
}
}
public void EnableAuthMigration(string trace)
{
_allowAuthMigration.Set();
lock (_authMigrationLock)
{
if (_authMigrationAutoReenableTask == null)
{
var refreshIntervalInMS = 60 * 1000;
#if DEBUG
// For L0, we will refresh faster
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL")))
{
refreshIntervalInMS = int.Parse(Environment.GetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL"));
}
#endif
_authMigrationAutoReenableTask = AuthMigrationAuthReenableAsync(TimeSpan.FromMilliseconds(refreshIntervalInMS), _authMigrationAutoReenableTaskCancellationTokenSource.Token);
}
}
_trace.Info($"Enable auth migration at {DateTime.UtcNow.ToString("O")}.");
AuthMigrationChanged?.Invoke(this, new AuthMigrationEventArgs(trace));
}
public void DeferAuthMigration(TimeSpan deferred, string trace)
{
_allowAuthMigration.Reset();
// defer migration for a while
lock (_authMigrationLock)
{
_deferredAuthMigrationTime = DateTime.UtcNow.Add(deferred);
}
_trace.Info($"Disabled auth migration until {_deferredAuthMigrationTime.ToString("O")}.");
AuthMigrationChanged?.Invoke(this, new AuthMigrationEventArgs(trace));
}
public void LoadDefaultUserAgents() public void LoadDefaultUserAgents()
{ {
if (string.IsNullOrEmpty(WebProxy.HttpProxyAddress) && string.IsNullOrEmpty(WebProxy.HttpsProxyAddress)) if (string.IsNullOrEmpty(WebProxy.HttpProxyAddress) && string.IsNullOrEmpty(WebProxy.HttpsProxyAddress))
@@ -549,6 +628,18 @@ namespace GitHub.Runner.Common
_loadContext.Unloading -= LoadContext_Unloading; _loadContext.Unloading -= LoadContext_Unloading;
_loadContext = null; _loadContext = null;
} }
if (_authMigrationAutoReenableTask != null)
{
_authMigrationAutoReenableTaskCancellationTokenSource?.Cancel();
}
if (_authMigrationAutoReenableTaskCancellationTokenSource != null)
{
_authMigrationAutoReenableTaskCancellationTokenSource?.Dispose();
_authMigrationAutoReenableTaskCancellationTokenSource = null;
}
_httpTraceSubscription?.Dispose(); _httpTraceSubscription?.Dispose();
_diagListenerSubscription?.Dispose(); _diagListenerSubscription?.Dispose();
_traceManager?.Dispose(); _traceManager?.Dispose();

View File

@@ -32,18 +32,6 @@ namespace GitHub.Runner.Common
string billingOwnerId, string billingOwnerId,
CancellationToken token); CancellationToken token);
Task CompleteJob2Async(
Guid planId,
Guid jobId,
TaskResult result,
Dictionary<String, VariableValue> outputs,
IList<StepResult> stepResults,
IList<Annotation> jobAnnotations,
string environmentUrl,
IList<Telemetry> telemetry,
string billingOwnerId,
CancellationToken token);
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token); Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
} }
@@ -82,7 +70,6 @@ namespace GitHub.Runner.Common
ex is not TaskOrchestrationJobUnprocessableException); // HTTP status 422 ex is not TaskOrchestrationJobUnprocessableException); // HTTP status 422
} }
// Legacy will be deleted when SkipRetryCompleteJobUponKnownErrors is cleaned up
public Task CompleteJobAsync( public Task CompleteJobAsync(
Guid planId, Guid planId,
Guid jobId, Guid jobId,
@@ -94,23 +81,6 @@ namespace GitHub.Runner.Common
IList<Telemetry> telemetry, IList<Telemetry> telemetry,
string billingOwnerId, string billingOwnerId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{
CheckConnection();
return RetryRequest(
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, environmentUrl, telemetry, billingOwnerId, cancellationToken), cancellationToken);
}
public Task CompleteJob2Async(
Guid planId,
Guid jobId,
TaskResult result,
Dictionary<String, VariableValue> outputs,
IList<StepResult> stepResults,
IList<Annotation> jobAnnotations,
string environmentUrl,
IList<Telemetry> telemetry,
string billingOwnerId,
CancellationToken cancellationToken)
{ {
CheckConnection(); CheckConnection();
return RetryRequest( return RetryRequest(
@@ -124,7 +94,9 @@ namespace GitHub.Runner.Common
{ {
CheckConnection(); CheckConnection();
return RetryRequest<RenewJobResponse>( return RetryRequest<RenewJobResponse>(
async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken); async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken,
shouldRetry: ex =>
ex is not TaskOrchestrationJobNotFoundException); // HTTP status 404
} }
} }
} }

View File

@@ -26,15 +26,31 @@ namespace GitHub.Runner.Listener
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online; private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
private CancellationTokenSource _getMessagesTokenSource; private CancellationTokenSource _getMessagesTokenSource;
private VssCredentials _creds; private VssCredentials _creds;
private VssCredentials _credsV2;
private TaskAgentSession _session; private TaskAgentSession _session;
private IRunnerServer _runnerServer; private IRunnerServer _runnerServer;
private IBrokerServer _brokerServer; private IBrokerServer _brokerServer;
private ICredentialManager _credMgr;
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new(); private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
private bool _accessTokenRevoked = false; private bool _accessTokenRevoked = false;
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30); private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4); private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30); private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
private bool _needRefreshCredsV2 = false;
private bool _handlerInitialized = false;
private bool _isMigratedSettings = false;
private const int _maxMigratedSettingsRetries = 3;
private int _migratedSettingsRetryCount = 0;
public BrokerMessageListener()
{
}
public BrokerMessageListener(RunnerSettings settings, bool isMigratedSettings = false)
{
_settings = settings;
_isMigratedSettings = isMigratedSettings;
}
public override void Initialize(IHostContext hostContext) public override void Initialize(IHostContext hostContext)
{ {
@@ -43,15 +59,29 @@ namespace GitHub.Runner.Listener
_term = HostContext.GetService<ITerminal>(); _term = HostContext.GetService<ITerminal>();
_runnerServer = HostContext.GetService<IRunnerServer>(); _runnerServer = HostContext.GetService<IRunnerServer>();
_brokerServer = HostContext.GetService<IBrokerServer>(); _brokerServer = HostContext.GetService<IBrokerServer>();
_credMgr = HostContext.GetService<ICredentialManager>();
} }
public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token) public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
{ {
Trace.Entering(); Trace.Entering();
// Settings // Load settings if not provided through constructor
var configManager = HostContext.GetService<IConfigurationManager>(); if (_settings == null)
_settings = configManager.LoadSettings(); {
var configManager = HostContext.GetService<IConfigurationManager>();
_settings = configManager.LoadSettings();
Trace.Info("Settings loaded from config manager");
}
else
{
Trace.Info("Using provided settings");
if (_isMigratedSettings)
{
Trace.Info("Using migrated settings from .runner_migrated");
}
}
var serverUrlV2 = _settings.ServerUrlV2; var serverUrlV2 = _settings.ServerUrlV2;
var serverUrl = _settings.ServerUrl; var serverUrl = _settings.ServerUrl;
Trace.Info(_settings); Trace.Info(_settings);
@@ -63,8 +93,7 @@ namespace GitHub.Runner.Listener
// Create connection. // Create connection.
Trace.Info("Loading Credentials"); Trace.Info("Loading Credentials");
var credMgr = HostContext.GetService<ICredentialManager>(); _creds = _credMgr.LoadCredentials(allowAuthUrlV2: false);
_creds = credMgr.LoadCredentials();
var agent = new TaskAgentReference var agent = new TaskAgentReference
{ {
@@ -87,7 +116,8 @@ namespace GitHub.Runner.Listener
try try
{ {
Trace.Info("Connecting to the Broker Server..."); Trace.Info("Connecting to the Broker Server...");
await _brokerServer.ConnectAsync(new Uri(serverUrlV2), _creds); _credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
await _brokerServer.ConnectAsync(new Uri(serverUrlV2), _credsV2);
Trace.Info("VssConnection created"); Trace.Info("VssConnection created");
if (!string.IsNullOrEmpty(serverUrl) && if (!string.IsNullOrEmpty(serverUrl) &&
@@ -112,6 +142,13 @@ namespace GitHub.Runner.Listener
encounteringError = false; encounteringError = false;
} }
if (!_handlerInitialized)
{
// Register event handler for auth migration state change
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
_handlerInitialized = true;
}
return CreateSessionResult.Success; return CreateSessionResult.Success;
} }
catch (OperationCanceledException) when (token.IsCancellationRequested) catch (OperationCanceledException) when (token.IsCancellationRequested)
@@ -130,7 +167,22 @@ 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 using migrated settings, limit the number of retries before returning failure
if (_isMigratedSettings)
{
_migratedSettingsRetryCount++;
Trace.Warning($"Migrated settings retry {_migratedSettingsRetryCount} of {_maxMigratedSettingsRetries}");
if (_migratedSettingsRetryCount >= _maxMigratedSettingsRetries)
{
Trace.Warning("Reached maximum retry attempts for migrated settings. Returning failure to try default settings.");
return CreateSessionResult.Failure;
}
}
if (!HostContext.AllowAuthMigration &&
ex is VssOAuthTokenRequestException vssOAuthEx &&
_credsV2.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))
@@ -151,7 +203,8 @@ namespace GitHub.Runner.Listener
} }
} }
if (!IsSessionCreationExceptionRetriable(ex)) if (!HostContext.AllowAuthMigration &&
!IsSessionCreationExceptionRetriable(ex))
{ {
_term.WriteError($"Failed to create session. {ex.Message}"); _term.WriteError($"Failed to create session. {ex.Message}");
if (ex is TaskAgentSessionConflictException) if (ex is TaskAgentSessionConflictException)
@@ -161,6 +214,12 @@ namespace GitHub.Runner.Listener
return CreateSessionResult.Failure; return CreateSessionResult.Failure;
} }
if (HostContext.AllowAuthMigration)
{
Trace.Info("Disable migration mode for 60 minutes.");
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Session creation failed with exception: {ex}");
}
if (!encounteringError) //print the message only on the first error if (!encounteringError) //print the message only on the first error
{ {
_term.WriteError($"{DateTime.UtcNow:u}: Runner connect error: {ex.Message}. Retrying until reconnected."); _term.WriteError($"{DateTime.UtcNow:u}: Runner connect error: {ex.Message}. Retrying until reconnected.");
@@ -177,6 +236,11 @@ namespace GitHub.Runner.Listener
{ {
if (_session != null && _session.SessionId != Guid.Empty) if (_session != null && _session.SessionId != Guid.Empty)
{ {
if (_handlerInitialized)
{
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
}
if (!_accessTokenRevoked) if (!_accessTokenRevoked)
{ {
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30))) using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
@@ -219,6 +283,13 @@ namespace GitHub.Runner.Listener
_getMessagesTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); _getMessagesTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
try try
{ {
if (_needRefreshCredsV2)
{
Trace.Info("Refreshing broker connection.");
await RefreshBrokerConnectionAsync();
_needRefreshCredsV2 = false;
}
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId, message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
runnerStatus, runnerStatus,
BuildConstants.RunnerPackage.Version, BuildConstants.RunnerPackage.Version,
@@ -254,11 +325,11 @@ namespace GitHub.Runner.Listener
Trace.Info("Hosted runner has been deprovisioned."); Trace.Info("Hosted runner has been deprovisioned.");
throw; throw;
} }
catch (AccessDeniedException e) when (e.ErrorCode == 1) catch (AccessDeniedException e) when (e.ErrorCode == 1 && !HostContext.AllowAuthMigration)
{ {
throw; throw;
} }
catch (RunnerNotFoundException) catch (RunnerNotFoundException) when (!HostContext.AllowAuthMigration)
{ {
throw; throw;
} }
@@ -267,7 +338,8 @@ namespace GitHub.Runner.Listener
Trace.Error("Catch exception during get next message."); Trace.Error("Catch exception during get next message.");
Trace.Error(ex); Trace.Error(ex);
if (!IsGetNextMessageExceptionRetriable(ex)) if (!HostContext.AllowAuthMigration &&
!IsGetNextMessageExceptionRetriable(ex))
{ {
throw new NonRetryableException("Get next message failed with non-retryable error.", ex); throw new NonRetryableException("Get next message failed with non-retryable error.", ex);
} }
@@ -298,6 +370,12 @@ namespace GitHub.Runner.Listener
encounteringError = true; encounteringError = true;
} }
if (HostContext.AllowAuthMigration)
{
Trace.Info("Disable migration mode for 60 minutes.");
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Get next message failed with exception: {ex}");
}
// re-create VssConnection before next retry // re-create VssConnection before next retry
await RefreshBrokerConnectionAsync(); await RefreshBrokerConnectionAsync();
@@ -329,7 +407,7 @@ namespace GitHub.Runner.Listener
} }
} }
public async Task RefreshListenerTokenAsync(CancellationToken cancellationToken) public async Task RefreshListenerTokenAsync()
{ {
await RefreshBrokerConnectionAsync(); await RefreshBrokerConnectionAsync();
} }
@@ -432,17 +510,16 @@ namespace GitHub.Runner.Listener
private async Task RefreshBrokerConnectionAsync() private async Task RefreshBrokerConnectionAsync()
{ {
var configManager = HostContext.GetService<IConfigurationManager>(); Trace.Info("Reload credentials.");
_settings = configManager.LoadSettings(); _credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
await _brokerServer.ConnectAsync(new Uri(_settings.ServerUrlV2), _credsV2);
Trace.Info("Connection to Broker Server recreated.");
}
if (string.IsNullOrEmpty(_settings.ServerUrlV2)) private void HandleAuthMigrationChanged(object sender, EventArgs e)
{ {
throw new InvalidOperationException("ServerUrlV2 is not set"); Trace.Info($"Auth migration changed. Current allow auth migration state: {HostContext.AllowAuthMigration}");
} _needRefreshCredsV2 = true;
var credMgr = HostContext.GetService<ICredentialManager>();
VssCredentials creds = credMgr.LoadCredentials();
await _brokerServer.ConnectAsync(new Uri(_settings.ServerUrlV2), creds);
} }
} }
} }

View File

@@ -25,6 +25,7 @@ namespace GitHub.Runner.Listener.Configuration
Task UnconfigureAsync(CommandSettings command); Task UnconfigureAsync(CommandSettings command);
void DeleteLocalRunnerConfig(); void DeleteLocalRunnerConfig();
RunnerSettings LoadSettings(); RunnerSettings LoadSettings();
RunnerSettings LoadMigratedSettings();
} }
public sealed class ConfigurationManager : RunnerService, IConfigurationManager public sealed class ConfigurationManager : RunnerService, IConfigurationManager
@@ -66,6 +67,22 @@ namespace GitHub.Runner.Listener.Configuration
return settings; return settings;
} }
public RunnerSettings LoadMigratedSettings()
{
Trace.Info(nameof(LoadMigratedSettings));
// Check if migrated settings file exists
if (!_store.IsMigratedConfigured())
{
throw new NonRetryableException("No migrated configuration found.");
}
RunnerSettings settings = _store.GetMigratedSettings();
Trace.Info("Migrated Settings Loaded");
return settings;
}
public async Task ConfigureAsync(CommandSettings command) public async Task ConfigureAsync(CommandSettings command)
{ {
_term.WriteLine(); _term.WriteLine();
@@ -127,7 +144,7 @@ namespace GitHub.Runner.Listener.Configuration
runnerSettings.ServerUrl = inputUrl; runnerSettings.ServerUrl = inputUrl;
// Get the credentials // Get the credentials
credProvider = GetCredentialProvider(command, runnerSettings.ServerUrl); credProvider = GetCredentialProvider(command, runnerSettings.ServerUrl);
creds = credProvider.GetVssCredentials(HostContext); creds = credProvider.GetVssCredentials(HostContext, allowAuthUrlV2: false);
Trace.Info("legacy vss cred retrieved"); Trace.Info("legacy vss cred retrieved");
} }
else else
@@ -366,7 +383,7 @@ namespace GitHub.Runner.Listener.Configuration
{ {
{ "clientId", agent.Authorization.ClientId.ToString("D") }, { "clientId", agent.Authorization.ClientId.ToString("D") },
{ "authorizationUrl", agent.Authorization.AuthorizationUrl.AbsoluteUri }, { "authorizationUrl", agent.Authorization.AuthorizationUrl.AbsoluteUri },
{ "requireFipsCryptography", agent.Properties.GetValue("RequireFipsCryptography", false).ToString() } { "requireFipsCryptography", agent.Properties.GetValue("RequireFipsCryptography", true).ToString() }
}, },
}; };
@@ -384,7 +401,7 @@ namespace GitHub.Runner.Listener.Configuration
if (!runnerSettings.UseV2Flow) if (!runnerSettings.UseV2Flow)
{ {
var credMgr = HostContext.GetService<ICredentialManager>(); var credMgr = HostContext.GetService<ICredentialManager>();
VssCredentials credential = credMgr.LoadCredentials(); VssCredentials credential = credMgr.LoadCredentials(allowAuthUrlV2: false);
try try
{ {
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential); await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
@@ -519,7 +536,7 @@ namespace GitHub.Runner.Listener.Configuration
if (string.IsNullOrEmpty(settings.GitHubUrl)) if (string.IsNullOrEmpty(settings.GitHubUrl))
{ {
var credProvider = GetCredentialProvider(command, settings.ServerUrl); var credProvider = GetCredentialProvider(command, settings.ServerUrl);
creds = credProvider.GetVssCredentials(HostContext); creds = credProvider.GetVssCredentials(HostContext, allowAuthUrlV2: false);
Trace.Info("legacy vss cred retrieved"); Trace.Info("legacy vss cred retrieved");
} }
else else

View File

@@ -13,7 +13,7 @@ namespace GitHub.Runner.Listener.Configuration
public interface ICredentialManager : IRunnerService public interface ICredentialManager : IRunnerService
{ {
ICredentialProvider GetCredentialProvider(string credType); ICredentialProvider GetCredentialProvider(string credType);
VssCredentials LoadCredentials(); VssCredentials LoadCredentials(bool allowAuthUrlV2);
} }
public class CredentialManager : RunnerService, ICredentialManager public class CredentialManager : RunnerService, ICredentialManager
@@ -40,7 +40,7 @@ namespace GitHub.Runner.Listener.Configuration
return creds; return creds;
} }
public VssCredentials LoadCredentials() public VssCredentials LoadCredentials(bool allowAuthUrlV2)
{ {
IConfigurationStore store = HostContext.GetService<IConfigurationStore>(); IConfigurationStore store = HostContext.GetService<IConfigurationStore>();
@@ -51,21 +51,16 @@ namespace GitHub.Runner.Listener.Configuration
CredentialData credData = store.GetCredentials(); CredentialData credData = store.GetCredentials();
var migratedCred = store.GetMigratedCredentials(); var migratedCred = store.GetMigratedCredentials();
if (migratedCred != null) if (migratedCred != null &&
migratedCred.Scheme == Constants.Configuration.OAuth)
{ {
credData = migratedCred; credData = migratedCred;
// Re-write .credentials with Token URL
store.SaveCredential(credData);
// Delete .credentials_migrated
store.DeleteMigratedCredential();
} }
ICredentialProvider credProv = GetCredentialProvider(credData.Scheme); ICredentialProvider credProv = GetCredentialProvider(credData.Scheme);
credProv.CredentialData = credData; credProv.CredentialData = credData;
VssCredentials creds = credProv.GetVssCredentials(HostContext); VssCredentials creds = credProv.GetVssCredentials(HostContext, allowAuthUrlV2);
return creds; return creds;
} }

View File

@@ -1,7 +1,7 @@
using System; using System;
using GitHub.Services.Common;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Services.Common;
using GitHub.Services.OAuth; using GitHub.Services.OAuth;
namespace GitHub.Runner.Listener.Configuration namespace GitHub.Runner.Listener.Configuration
@@ -10,7 +10,7 @@ namespace GitHub.Runner.Listener.Configuration
{ {
Boolean RequireInteractive { get; } Boolean RequireInteractive { get; }
CredentialData CredentialData { get; set; } CredentialData CredentialData { get; set; }
VssCredentials GetVssCredentials(IHostContext context); VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2);
void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl); void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
} }
@@ -25,7 +25,7 @@ namespace GitHub.Runner.Listener.Configuration
public virtual Boolean RequireInteractive => false; public virtual Boolean RequireInteractive => false;
public CredentialData CredentialData { get; set; } public CredentialData CredentialData { get; set; }
public abstract VssCredentials GetVssCredentials(IHostContext context); public abstract VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2);
public abstract void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl); public abstract void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
} }
@@ -33,7 +33,7 @@ namespace GitHub.Runner.Listener.Configuration
{ {
public OAuthAccessTokenCredential() : base(Constants.Configuration.OAuthAccessToken) { } public OAuthAccessTokenCredential() : base(Constants.Configuration.OAuthAccessToken) { }
public override VssCredentials GetVssCredentials(IHostContext context) public override VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2)
{ {
ArgUtil.NotNull(context, nameof(context)); ArgUtil.NotNull(context, nameof(context));
Tracing trace = context.GetTrace(nameof(OAuthAccessTokenCredential)); Tracing trace = context.GetTrace(nameof(OAuthAccessTokenCredential));

View File

@@ -22,10 +22,18 @@ namespace GitHub.Runner.Listener.Configuration
// Nothing to verify here // Nothing to verify here
} }
public override VssCredentials GetVssCredentials(IHostContext context) public override VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2)
{ {
var clientId = this.CredentialData.Data.GetValueOrDefault("clientId", null); var clientId = this.CredentialData.Data.GetValueOrDefault("clientId", null);
var authorizationUrl = this.CredentialData.Data.GetValueOrDefault("authorizationUrl", null); var authorizationUrl = this.CredentialData.Data.GetValueOrDefault("authorizationUrl", null);
var authorizationUrlV2 = this.CredentialData.Data.GetValueOrDefault("authorizationUrlV2", null);
if (allowAuthUrlV2 &&
!string.IsNullOrEmpty(authorizationUrlV2) &&
context.AllowAuthMigration)
{
authorizationUrl = authorizationUrlV2;
}
// For back compat with .credential file that doesn't has 'oauthEndpointUrl' section // For back compat with .credential file that doesn't has 'oauthEndpointUrl' section
var oauthEndpointUrl = this.CredentialData.Data.GetValueOrDefault("oauthEndpointUrl", authorizationUrl); var oauthEndpointUrl = this.CredentialData.Data.GetValueOrDefault("oauthEndpointUrl", authorizationUrl);

View File

@@ -33,7 +33,7 @@ namespace GitHub.Runner.Listener
Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token); Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token);
Task DeleteMessageAsync(TaskAgentMessage message); Task DeleteMessageAsync(TaskAgentMessage message);
Task RefreshListenerTokenAsync(CancellationToken token); Task RefreshListenerTokenAsync();
void OnJobStatus(object sender, JobStatusEventArgs e); void OnJobStatus(object sender, JobStatusEventArgs e);
} }
@@ -44,6 +44,7 @@ namespace GitHub.Runner.Listener
private ITerminal _term; private ITerminal _term;
private IRunnerServer _runnerServer; private IRunnerServer _runnerServer;
private IBrokerServer _brokerServer; private IBrokerServer _brokerServer;
private ICredentialManager _credMgr;
private TaskAgentSession _session; private TaskAgentSession _session;
private TimeSpan _getNextMessageRetryInterval; private TimeSpan _getNextMessageRetryInterval;
private bool _accessTokenRevoked = false; private bool _accessTokenRevoked = false;
@@ -54,8 +55,9 @@ namespace GitHub.Runner.Listener
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online; private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
private CancellationTokenSource _getMessagesTokenSource; private CancellationTokenSource _getMessagesTokenSource;
private VssCredentials _creds; private VssCredentials _creds;
private VssCredentials _credsV2;
private bool _isBrokerSession = false; private bool _needRefreshCredsV2 = false;
private bool _handlerInitialized = false;
public override void Initialize(IHostContext hostContext) public override void Initialize(IHostContext hostContext)
{ {
@@ -64,6 +66,7 @@ namespace GitHub.Runner.Listener
_term = HostContext.GetService<ITerminal>(); _term = HostContext.GetService<ITerminal>();
_runnerServer = HostContext.GetService<IRunnerServer>(); _runnerServer = HostContext.GetService<IRunnerServer>();
_brokerServer = hostContext.GetService<IBrokerServer>(); _brokerServer = hostContext.GetService<IBrokerServer>();
_credMgr = hostContext.GetService<ICredentialManager>();
} }
public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token) public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
@@ -78,8 +81,7 @@ namespace GitHub.Runner.Listener
// Create connection. // Create connection.
Trace.Info("Loading Credentials"); Trace.Info("Loading Credentials");
var credMgr = HostContext.GetService<ICredentialManager>(); _creds = _credMgr.LoadCredentials(allowAuthUrlV2: false);
_creds = credMgr.LoadCredentials();
var agent = new TaskAgentReference var agent = new TaskAgentReference
{ {
@@ -113,16 +115,6 @@ namespace GitHub.Runner.Listener
_settings.PoolId, _settings.PoolId,
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)
{ {
@@ -131,6 +123,13 @@ namespace GitHub.Runner.Listener
encounteringError = false; encounteringError = false;
} }
if (!_handlerInitialized)
{
Trace.Info("Registering AuthMigrationChanged event handler.");
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
_handlerInitialized = true;
}
return CreateSessionResult.Success; return CreateSessionResult.Success;
} }
catch (OperationCanceledException) when (token.IsCancellationRequested) catch (OperationCanceledException) when (token.IsCancellationRequested)
@@ -196,16 +195,16 @@ namespace GitHub.Runner.Listener
{ {
if (_session != null && _session.SessionId != Guid.Empty) if (_session != null && _session.SessionId != Guid.Empty)
{ {
if (_handlerInitialized)
{
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
}
if (!_accessTokenRevoked) if (!_accessTokenRevoked)
{ {
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
@@ -261,12 +260,19 @@ 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) if (message != null && message.MessageType == BrokerMigrationMessage.MessageType)
{ {
var migrationMessage = JsonUtility.FromString<BrokerMigrationMessage>(message.Body); var migrationMessage = JsonUtility.FromString<BrokerMigrationMessage>(message.Body);
await _brokerServer.UpdateConnectionIfNeeded(migrationMessage.BrokerBaseUrl, _creds); _credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
await _brokerServer.UpdateConnectionIfNeeded(migrationMessage.BrokerBaseUrl, _credsV2);
if (_needRefreshCredsV2)
{
Trace.Info("Refreshing credentials for V2.");
await _brokerServer.ForceRefreshConnection(_credsV2);
_needRefreshCredsV2 = false;
}
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId, message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
runnerStatus, runnerStatus,
BuildConstants.RunnerPackage.Version, BuildConstants.RunnerPackage.Version,
@@ -309,11 +315,11 @@ namespace GitHub.Runner.Listener
Trace.Info("Hosted runner has been deprovisioned."); Trace.Info("Hosted runner has been deprovisioned.");
throw; throw;
} }
catch (AccessDeniedException e) when (e.ErrorCode == 1) catch (AccessDeniedException e) when (e.ErrorCode == 1 && !HostContext.AllowAuthMigration)
{ {
throw; throw;
} }
catch (RunnerNotFoundException) catch (RunnerNotFoundException) when (!HostContext.AllowAuthMigration)
{ {
throw; throw;
} }
@@ -322,12 +328,19 @@ namespace GitHub.Runner.Listener
Trace.Error("Catch exception during get next message."); Trace.Error("Catch exception during get next message.");
Trace.Error(ex); Trace.Error(ex);
// clear out potential message for broker migration,
// in case the exception is thrown from get message from broker-listener.
message = null;
// 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) == CreateSessionResult.Success)) if (!HostContext.AllowAuthMigration &&
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.");
} }
else if (!IsGetNextMessageExceptionRetriable(ex)) else if (!HostContext.AllowAuthMigration &&
!IsGetNextMessageExceptionRetriable(ex))
{ {
throw; throw;
} }
@@ -354,6 +367,12 @@ namespace GitHub.Runner.Listener
encounteringError = true; encounteringError = true;
} }
if (HostContext.AllowAuthMigration)
{
Trace.Info("Disable migration mode for 60 minutes.");
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Get next message failed with exception: {ex}");
}
// re-create VssConnection before next retry // re-create VssConnection before next retry
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60)); await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
@@ -411,10 +430,11 @@ namespace GitHub.Runner.Listener
} }
} }
public async Task RefreshListenerTokenAsync(CancellationToken cancellationToken) public async Task RefreshListenerTokenAsync()
{ {
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60)); await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
await _brokerServer.ForceRefreshConnection(_creds); _credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
await _brokerServer.ForceRefreshConnection(_credsV2);
} }
private TaskAgentMessage DecryptMessage(TaskAgentMessage message) private TaskAgentMessage DecryptMessage(TaskAgentMessage message)
@@ -545,5 +565,11 @@ namespace GitHub.Runner.Listener
return true; return true;
} }
} }
private void HandleAuthMigrationChanged(object sender, EventArgs e)
{
Trace.Info($"Auth migration changed. Current allow auth migration state: {HostContext.AllowAuthMigration}");
_needRefreshCredsV2 = true;
}
} }
} }

View File

@@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" /> <PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" /> <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" /> <PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -14,7 +16,9 @@ using GitHub.Runner.Common.Util;
using GitHub.Runner.Listener.Check; using GitHub.Runner.Listener.Check;
using GitHub.Runner.Listener.Configuration; using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Services.OAuth;
using GitHub.Services.WebApi; using GitHub.Services.WebApi;
using GitHub.Services.WebApi.Jwt;
using Pipelines = GitHub.DistributedTask.Pipelines; using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Listener namespace GitHub.Runner.Listener
@@ -31,6 +35,14 @@ namespace GitHub.Runner.Listener
private ITerminal _term; private ITerminal _term;
private bool _inConfigStage; private bool _inConfigStage;
private ManualResetEvent _completedCommand = new(false); private ManualResetEvent _completedCommand = new(false);
private readonly ConcurrentQueue<string> _authMigrationTelemetries = new();
private Task _authMigrationTelemetryTask;
private readonly object _authMigrationTelemetryLock = new();
private Task _authMigrationClaimsCheckTask;
private readonly object _authMigrationClaimsCheckLock = new();
private IRunnerServer _runnerServer;
private CancellationTokenSource _authMigrationTelemetryTokenSource = new();
private CancellationTokenSource _authMigrationClaimsCheckTokenSource = new();
// <summary> // <summary>
// Helps avoid excessive calls to Run Service when encountering non-retriable errors from /acquirejob. // Helps avoid excessive calls to Run Service when encountering non-retriable errors from /acquirejob.
@@ -51,6 +63,7 @@ namespace GitHub.Runner.Listener
base.Initialize(hostContext); base.Initialize(hostContext);
_term = HostContext.GetService<ITerminal>(); _term = HostContext.GetService<ITerminal>();
_acquireJobThrottler = HostContext.CreateService<IErrorThrottler>(); _acquireJobThrottler = HostContext.CreateService<IErrorThrottler>();
_runnerServer = HostContext.GetService<IRunnerServer>();
} }
public async Task<int> ExecuteCommand(CommandSettings command) public async Task<int> ExecuteCommand(CommandSettings command)
@@ -66,6 +79,8 @@ namespace GitHub.Runner.Listener
//register a SIGTERM handler //register a SIGTERM handler
HostContext.Unloading += Runner_Unloading; HostContext.Unloading += Runner_Unloading;
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
// TODO Unit test to cover this logic // TODO Unit test to cover this logic
Trace.Info(nameof(ExecuteCommand)); Trace.Info(nameof(ExecuteCommand));
var configManager = HostContext.GetService<IConfigurationManager>(); var configManager = HostContext.GetService<IConfigurationManager>();
@@ -300,8 +315,17 @@ namespace GitHub.Runner.Listener
_term.WriteLine("https://docs.github.com/en/actions/hosting-your-own-runners/autoscaling-with-self-hosted-runners#using-ephemeral-runners-for-autoscaling", ConsoleColor.Yellow); _term.WriteLine("https://docs.github.com/en/actions/hosting-your-own-runners/autoscaling-with-self-hosted-runners#using-ephemeral-runners-for-autoscaling", ConsoleColor.Yellow);
} }
var cred = store.GetCredentials();
if (cred != null &&
cred.Scheme == Constants.Configuration.OAuth &&
cred.Data.ContainsKey("EnableAuthMigrationByDefault"))
{
Trace.Info("Enable auth migration by default.");
HostContext.EnableAuthMigration("EnableAuthMigrationByDefault");
}
// Run the runner interactively or as service // Run the runner interactively or as service
return await RunAsync(settings, command.RunOnce || settings.Ephemeral); return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral);
} }
else else
{ {
@@ -311,6 +335,9 @@ namespace GitHub.Runner.Listener
} }
finally finally
{ {
_authMigrationClaimsCheckTokenSource?.Cancel();
_authMigrationTelemetryTokenSource?.Cancel();
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
_term.CancelKeyPress -= CtrlCHandler; _term.CancelKeyPress -= CtrlCHandler;
HostContext.Unloading -= Runner_Unloading; HostContext.Unloading -= Runner_Unloading;
_completedCommand.Set(); _completedCommand.Set();
@@ -360,12 +387,12 @@ namespace GitHub.Runner.Listener
} }
} }
private IMessageListener GetMessageListener(RunnerSettings settings) private IMessageListener GetMessageListener(RunnerSettings settings, bool isMigratedSettings = false)
{ {
if (settings.UseV2Flow) if (settings.UseV2Flow)
{ {
Trace.Info($"Using BrokerMessageListener"); Trace.Info($"Using BrokerMessageListener");
var brokerListener = new BrokerMessageListener(); var brokerListener = new BrokerMessageListener(settings, isMigratedSettings);
brokerListener.Initialize(HostContext); brokerListener.Initialize(HostContext);
return brokerListener; return brokerListener;
} }
@@ -379,15 +406,65 @@ namespace GitHub.Runner.Listener
try try
{ {
Trace.Info(nameof(RunAsync)); Trace.Info(nameof(RunAsync));
_listener = GetMessageListener(settings);
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken); // First try using migrated settings if available
if (createSessionResult == CreateSessionResult.SessionConflict) var configManager = HostContext.GetService<IConfigurationManager>();
RunnerSettings migratedSettings = null;
try
{ {
return Constants.Runner.ReturnCode.SessionConflict; migratedSettings = configManager.LoadMigratedSettings();
Trace.Info("Loaded migrated settings from .runner_migrated file");
Trace.Info(migratedSettings);
} }
else if (createSessionResult == CreateSessionResult.Failure) catch (Exception ex)
{ {
return Constants.Runner.ReturnCode.TerminatedError; // If migrated settings file doesn't exist or can't be loaded, we'll use the provided settings
Trace.Info($"Failed to load migrated settings: {ex.Message}");
}
bool usedMigratedSettings = false;
if (migratedSettings != null)
{
// Try to create session with migrated settings first
Trace.Info("Attempting to create session using migrated settings");
_listener = GetMessageListener(migratedSettings, isMigratedSettings: true);
try
{
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
if (createSessionResult == CreateSessionResult.Success)
{
Trace.Info("Successfully created session with migrated settings");
settings = migratedSettings; // Use migrated settings for the rest of the process
usedMigratedSettings = true;
}
else
{
Trace.Warning($"Failed to create session with migrated settings: {createSessionResult}");
}
}
catch (Exception ex)
{
Trace.Error($"Exception when creating session with migrated settings: {ex}");
}
}
// If migrated settings weren't used or session creation failed, use original settings
if (!usedMigratedSettings)
{
Trace.Info("Falling back to original .runner settings");
_listener = GetMessageListener(settings);
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;
}
} }
HostContext.WritePerfCounter("SessionCreated"); HostContext.WritePerfCounter("SessionCreated");
@@ -401,6 +478,8 @@ namespace GitHub.Runner.Listener
// Should we try to cleanup ephemeral runners // Should we try to cleanup ephemeral runners
bool runOnceJobCompleted = false; bool runOnceJobCompleted = false;
bool skipSessionDeletion = false; bool skipSessionDeletion = false;
bool restartSession = false; // Flag to indicate session restart
bool restartSessionPending = false;
try try
{ {
var notification = HostContext.GetService<IJobNotification>(); var notification = HostContext.GetService<IJobNotification>();
@@ -416,6 +495,15 @@ namespace GitHub.Runner.Listener
while (!HostContext.RunnerShutdownToken.IsCancellationRequested) while (!HostContext.RunnerShutdownToken.IsCancellationRequested)
{ {
// Check if we need to restart the session and can do so (job dispatcher not busy)
if (restartSessionPending && !jobDispatcher.Busy)
{
Trace.Info("Pending session restart detected and job dispatcher is not busy. Restarting session now.");
messageQueueLoopTokenSource.Cancel();
restartSession = true;
break;
}
TaskAgentMessage message = null; TaskAgentMessage message = null;
bool skipMessageDeletion = false; bool skipMessageDeletion = false;
try try
@@ -570,18 +658,18 @@ namespace GitHub.Runner.Listener
// Create connection // Create connection
var credMgr = HostContext.GetService<ICredentialManager>(); var credMgr = HostContext.GetService<ICredentialManager>();
var creds = credMgr.LoadCredentials();
if (string.IsNullOrEmpty(messageRef.RunServiceUrl)) if (string.IsNullOrEmpty(messageRef.RunServiceUrl))
{ {
var creds = credMgr.LoadCredentials(allowAuthUrlV2: false);
var actionsRunServer = HostContext.CreateService<IActionsRunServer>(); var actionsRunServer = HostContext.CreateService<IActionsRunServer>();
await actionsRunServer.ConnectAsync(new Uri(settings.ServerUrl), creds); await actionsRunServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
jobRequestMessage = await actionsRunServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token); jobRequestMessage = await actionsRunServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
} }
else else
{ {
var credsV2 = credMgr.LoadCredentials(allowAuthUrlV2: true);
var runServer = HostContext.CreateService<IRunServer>(); var runServer = HostContext.CreateService<IRunServer>();
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), creds); await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), credsV2);
try try
{ {
jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageRef.BillingOwnerId, messageQueueLoopTokenSource.Token); jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageRef.BillingOwnerId, messageQueueLoopTokenSource.Token);
@@ -599,6 +687,13 @@ namespace GitHub.Runner.Listener
catch (Exception ex) catch (Exception ex)
{ {
Trace.Error($"Caught exception from acquiring job message: {ex}"); Trace.Error($"Caught exception from acquiring job message: {ex}");
if (HostContext.AllowAuthMigration)
{
Trace.Info("Disable migration mode for 60 minutes.");
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Acquire job failed with exception: {ex}");
}
continue; continue;
} }
} }
@@ -633,7 +728,7 @@ namespace GitHub.Runner.Listener
else if (string.Equals(message.MessageType, TaskAgentMessageTypes.ForceTokenRefresh)) else if (string.Equals(message.MessageType, TaskAgentMessageTypes.ForceTokenRefresh))
{ {
Trace.Info("Received ForceTokenRefreshMessage"); Trace.Info("Received ForceTokenRefreshMessage");
await _listener.RefreshListenerTokenAsync(messageQueueLoopTokenSource.Token); await _listener.RefreshListenerTokenAsync();
} }
else if (string.Equals(message.MessageType, RunnerRefreshConfigMessage.MessageType)) else if (string.Equals(message.MessageType, RunnerRefreshConfigMessage.MessageType))
{ {
@@ -645,6 +740,17 @@ namespace GitHub.Runner.Listener
configType: runnerRefreshConfigMessage.ConfigType, configType: runnerRefreshConfigMessage.ConfigType,
serviceType: runnerRefreshConfigMessage.ServiceType, serviceType: runnerRefreshConfigMessage.ServiceType,
configRefreshUrl: runnerRefreshConfigMessage.ConfigRefreshUrl); configRefreshUrl: runnerRefreshConfigMessage.ConfigRefreshUrl);
// Set flag to schedule session restart if ConfigType is "runner"
if (string.Equals(runnerRefreshConfigMessage.ConfigType, "runner", StringComparison.OrdinalIgnoreCase))
{
Trace.Info("Runner configuration was updated. Session restart has been scheduled");
restartSessionPending = true;
}
else
{
Trace.Info($"No session restart needed for config type: {runnerRefreshConfigMessage.ConfigType}");
}
} }
else else
{ {
@@ -699,10 +805,16 @@ namespace GitHub.Runner.Listener
if (settings.Ephemeral && runOnceJobCompleted) if (settings.Ephemeral && runOnceJobCompleted)
{ {
var configManager = HostContext.GetService<IConfigurationManager>();
configManager.DeleteLocalRunnerConfig(); configManager.DeleteLocalRunnerConfig();
} }
} }
// After cleanup, check if we need to restart the session
if (restartSession)
{
Trace.Info("Restarting runner session after config update...");
return Constants.Runner.ReturnCode.RunnerConfigurationRefreshed;
}
} }
catch (TaskAgentAccessTokenExpiredException) catch (TaskAgentAccessTokenExpiredException)
{ {
@@ -716,6 +828,220 @@ namespace GitHub.Runner.Listener
return Constants.Runner.ReturnCode.Success; return Constants.Runner.ReturnCode.Success;
} }
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce)
{
int returnCode = Constants.Runner.ReturnCode.Success;
bool restart = false;
do
{
restart = false;
returnCode = await RunAsync(settings, runOnce);
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
{
Trace.Info("Runner configuration was refreshed, restarting session...");
// Reload settings in case they changed
var configManager = HostContext.GetService<IConfigurationManager>();
settings = configManager.LoadSettings();
restart = true;
}
} while (restart);
return returnCode;
}
private void HandleAuthMigrationChanged(object sender, AuthMigrationEventArgs e)
{
Trace.Verbose("Handle AuthMigrationChanged in Runner");
_authMigrationTelemetries.Enqueue($"{DateTime.UtcNow.ToString("O")}: {e.Trace}");
// only start the telemetry reporting task once auth migration is changed (enabled or disabled)
lock (_authMigrationTelemetryLock)
{
if (_authMigrationTelemetryTask == null)
{
_authMigrationTelemetryTask = ReportAuthMigrationTelemetryAsync(_authMigrationTelemetryTokenSource.Token);
}
}
// only start the claims check task once auth migration is changed (enabled or disabled)
lock (_authMigrationClaimsCheckLock)
{
if (_authMigrationClaimsCheckTask == null)
{
_authMigrationClaimsCheckTask = CheckOAuthTokenClaimsAsync(_authMigrationClaimsCheckTokenSource.Token);
}
}
}
private async Task CheckOAuthTokenClaimsAsync(CancellationToken token)
{
string[] expectedClaims =
[
"owner_id",
"runner_id",
"runner_group_id",
"scale_set_id",
"is_ephemeral",
"labels"
];
try
{
var credMgr = HostContext.GetService<ICredentialManager>();
while (!token.IsCancellationRequested)
{
try
{
await HostContext.Delay(TimeSpan.FromMinutes(100), token);
}
catch (TaskCanceledException)
{
// Ignore cancellation
}
if (token.IsCancellationRequested)
{
break;
}
if (!HostContext.AllowAuthMigration)
{
Trace.Info("Skip checking oauth token claims since auth migration is disabled.");
continue;
}
var baselineCred = credMgr.LoadCredentials(allowAuthUrlV2: false);
var authV2Cred = credMgr.LoadCredentials(allowAuthUrlV2: true);
if (!(baselineCred.Federated is VssOAuthCredential baselineVssOAuthCred) ||
!(authV2Cred.Federated is VssOAuthCredential vssOAuthCredV2) ||
baselineVssOAuthCred == null ||
vssOAuthCredV2 == null)
{
Trace.Info("Skip checking oauth token claims for non-oauth credentials");
continue;
}
if (string.Equals(baselineVssOAuthCred.AuthorizationUrl.AbsoluteUri, vssOAuthCredV2.AuthorizationUrl.AbsoluteUri, StringComparison.OrdinalIgnoreCase))
{
Trace.Info("Skip checking oauth token claims for same authorization url");
continue;
}
var baselineProvider = baselineVssOAuthCred.GetTokenProvider(baselineVssOAuthCred.AuthorizationUrl);
var v2Provider = vssOAuthCredV2.GetTokenProvider(vssOAuthCredV2.AuthorizationUrl);
try
{
using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
using (var requestTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutTokenSource.Token))
{
var baselineToken = await baselineProvider.GetTokenAsync(null, requestTokenSource.Token);
var v2Token = await v2Provider.GetTokenAsync(null, requestTokenSource.Token);
if (baselineToken is VssOAuthAccessToken baselineAccessToken &&
v2Token is VssOAuthAccessToken v2AccessToken &&
!string.IsNullOrEmpty(baselineAccessToken.Value) &&
!string.IsNullOrEmpty(v2AccessToken.Value))
{
var baselineJwt = JsonWebToken.Create(baselineAccessToken.Value);
var baselineClaims = baselineJwt.ExtractClaims();
var v2Jwt = JsonWebToken.Create(v2AccessToken.Value);
var v2Claims = v2Jwt.ExtractClaims();
// Log extracted claims for debugging
Trace.Verbose($"Baseline token expected claims: {string.Join(", ", baselineClaims
.Where(c => expectedClaims.Contains(c.Type.ToLowerInvariant()))
.Select(c => $"{c.Type}:{c.Value}"))}");
Trace.Verbose($"V2 token expected claims: {string.Join(", ", v2Claims
.Where(c => expectedClaims.Contains(c.Type.ToLowerInvariant()))
.Select(c => $"{c.Type}:{c.Value}"))}");
foreach (var claim in expectedClaims)
{
// if baseline has the claim, v2 should have it too with exactly same value.
if (baselineClaims.FirstOrDefault(c => c.Type.ToLowerInvariant() == claim) is Claim baselineClaim &&
!string.IsNullOrEmpty(baselineClaim?.Value))
{
var v2Claim = v2Claims.FirstOrDefault(c => c.Type.ToLowerInvariant() == claim);
if (v2Claim?.Value != baselineClaim.Value)
{
Trace.Info($"Token Claim mismatch between two issuers. Expected: {baselineClaim.Type}:{baselineClaim.Value}. Actual: {v2Claim?.Type ?? "Empty"}:{v2Claim?.Value ?? "Empty"}");
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Expected claim {baselineClaim.Type}:{baselineClaim.Value} does not match {v2Claim?.Type ?? "Empty"}:{v2Claim?.Value ?? "Empty"}");
break;
}
}
}
Trace.Info("OAuth token claims check passed.");
}
}
}
catch (Exception ex)
{
Trace.Error("Failed to fetch and check OAuth token claims.");
Trace.Error(ex);
}
}
}
catch (Exception ex)
{
Trace.Error("Failed to check OAuth token claims in background.");
Trace.Error(ex);
}
}
private async Task ReportAuthMigrationTelemetryAsync(CancellationToken token)
{
var configManager = HostContext.GetService<IConfigurationManager>();
var runnerSettings = configManager.LoadSettings();
while (!token.IsCancellationRequested)
{
try
{
await HostContext.Delay(TimeSpan.FromSeconds(60), token);
}
catch (TaskCanceledException)
{
// Ignore cancellation
}
Trace.Verbose("Checking for auth migration telemetry to report");
while (_authMigrationTelemetries.TryDequeue(out var telemetry))
{
Trace.Verbose($"Reporting auth migration telemetry: {telemetry}");
if (runnerSettings != null)
{
try
{
using (var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{
await _runnerServer.UpdateAgentUpdateStateAsync(runnerSettings.PoolId, runnerSettings.AgentId, "RefreshConfig", telemetry, tokenSource.Token);
}
}
catch (Exception ex)
{
Trace.Error("Failed to report auth migration telemetry.");
Trace.Error(ex);
_authMigrationTelemetries.Enqueue(telemetry);
}
}
if (!token.IsCancellationRequested)
{
try
{
await HostContext.Delay(TimeSpan.FromSeconds(10), token);
}
catch (TaskCanceledException)
{
// Ignore cancellation
}
}
}
}
}
private void PrintUsage(CommandSettings command) private void PrintUsage(CommandSettings command)
{ {
string separator; string separator;

View File

@@ -197,11 +197,31 @@ namespace GitHub.Runner.Listener
await ReportTelemetryAsync($"Credential clientId in refreshed config '{refreshedClientId ?? "Empty"}' does not match the current credential clientId '{clientId}'."); await ReportTelemetryAsync($"Credential clientId in refreshed config '{refreshedClientId ?? "Empty"}' does not match the current credential clientId '{clientId}'.");
return; return;
} }
// make sure the credential authorizationUrl in the refreshed config match the current credential authorizationUrl for OAuth auth scheme
var authorizationUrl = _credData.Data.GetValueOrDefault("authorizationUrl", null);
var refreshedAuthorizationUrl = refreshedCredConfig.Data.GetValueOrDefault("authorizationUrl", null);
if (authorizationUrl != refreshedAuthorizationUrl)
{
Trace.Error($"Credential authorizationUrl in refreshed config '{refreshedAuthorizationUrl ?? "Empty"}' does not match the current credential authorizationUrl '{authorizationUrl}'.");
await ReportTelemetryAsync($"Credential authorizationUrl in refreshed config '{refreshedAuthorizationUrl ?? "Empty"}' does not match the current credential authorizationUrl '{authorizationUrl}'.");
return;
}
} }
// save the refreshed runner credentials as a separate file // save the refreshed runner credentials as a separate file
_store.SaveMigratedCredential(refreshedCredConfig); _store.SaveMigratedCredential(refreshedCredConfig);
await ReportTelemetryAsync("Runner credentials updated successfully.");
if (refreshedCredConfig.Data.ContainsKey("authorizationUrlV2"))
{
HostContext.EnableAuthMigration("Credential file updated");
await ReportTelemetryAsync("Runner credentials updated successfully. Auth migration is enabled.");
}
else
{
HostContext.DeferAuthMigration(TimeSpan.FromDays(365), "Credential file does not contain authorizationUrlV2");
await ReportTelemetryAsync("Runner credentials updated successfully. Auth migration is disabled.");
}
} }
private async Task<bool> VerifyRunnerQualifiedId(string runnerQualifiedId) private async Task<bool> VerifyRunnerQualifiedId(string runnerQualifiedId)

View File

@@ -862,7 +862,21 @@ namespace GitHub.Runner.Worker
ExpressionValues["secrets"] = Global.Variables.ToSecretsContext(); ExpressionValues["secrets"] = Global.Variables.ToSecretsContext();
ExpressionValues["runner"] = new RunnerContext(); ExpressionValues["runner"] = new RunnerContext();
ExpressionValues["job"] = new JobContext();
Trace.Info("Initializing Job context");
var jobContext = new JobContext();
if (Global.Variables.GetBoolean(Constants.Runner.Features.AddCheckRunIdToJobContext) ?? false)
{
ExpressionValues.TryGetValue("job", out var jobDictionary);
if (jobDictionary != null)
{
foreach (var pair in jobDictionary.AssertDictionary("job"))
{
jobContext[pair.Key] = pair.Value;
}
}
}
ExpressionValues["job"] = jobContext;
Trace.Info("Initialize GitHub context"); Trace.Info("Initialize GitHub context");
var githubAccessToken = new StringContextData(Global.Variables.Get("system.github.token")); var githubAccessToken = new StringContextData(Global.Variables.Get("system.github.token"));

View File

@@ -21,6 +21,7 @@ namespace GitHub.Runner.Worker
public sealed class IssueMatcher public sealed class IssueMatcher
{ {
private string _defaultSeverity; private string _defaultSeverity;
private string _defaultFromPath;
private string _owner; private string _owner;
private IssuePattern[] _patterns; private IssuePattern[] _patterns;
private IssueMatch[] _state; private IssueMatch[] _state;
@@ -29,6 +30,7 @@ namespace GitHub.Runner.Worker
{ {
_owner = config.Owner; _owner = config.Owner;
_defaultSeverity = config.Severity; _defaultSeverity = config.Severity;
_defaultFromPath = config.FromPath;
_patterns = config.Patterns.Select(x => new IssuePattern(x, timeout)).ToArray(); _patterns = config.Patterns.Select(x => new IssuePattern(x, timeout)).ToArray();
Reset(); Reset();
} }
@@ -59,6 +61,19 @@ namespace GitHub.Runner.Worker
} }
} }
public string DefaultFromPath
{
get
{
if (_defaultFromPath == null)
{
_defaultFromPath = string.Empty;
}
return _defaultFromPath;
}
}
public IssueMatch Match(string line) public IssueMatch Match(string line)
{ {
// Single pattern // Single pattern
@@ -69,7 +84,7 @@ namespace GitHub.Runner.Worker
if (regexMatch.Success) if (regexMatch.Success)
{ {
return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity); return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
} }
return null; return null;
@@ -110,7 +125,7 @@ namespace GitHub.Runner.Worker
} }
// Return // Return
return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity); return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
} }
// Not the last pattern // Not the last pattern
else else
@@ -184,7 +199,7 @@ namespace GitHub.Runner.Worker
public sealed class IssueMatch public sealed class IssueMatch
{ {
public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null) public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null, string defaultFromPath = null)
{ {
File = runningMatch?.File ?? GetValue(groups, pattern.File); File = runningMatch?.File ?? GetValue(groups, pattern.File);
Line = runningMatch?.Line ?? GetValue(groups, pattern.Line); Line = runningMatch?.Line ?? GetValue(groups, pattern.Line);
@@ -198,6 +213,11 @@ namespace GitHub.Runner.Worker
{ {
Severity = defaultSeverity; Severity = defaultSeverity;
} }
if (string.IsNullOrEmpty(FromPath) && !string.IsNullOrEmpty(defaultFromPath))
{
FromPath = defaultFromPath;
}
} }
public string File { get; } public string File { get; }
@@ -282,6 +302,9 @@ namespace GitHub.Runner.Worker
[DataMember(Name = "pattern")] [DataMember(Name = "pattern")]
private IssuePatternConfig[] _patterns; private IssuePatternConfig[] _patterns;
[DataMember(Name = "fromPath")]
private string _fromPath;
public string Owner public string Owner
{ {
get get
@@ -318,6 +341,24 @@ namespace GitHub.Runner.Worker
} }
} }
public string FromPath
{
get
{
if (_fromPath == null)
{
_fromPath = string.Empty;
}
return _fromPath;
}
set
{
_fromPath = value;
}
}
public IssuePatternConfig[] Patterns public IssuePatternConfig[] Patterns
{ {
get get

View File

@@ -1,4 +1,4 @@
using GitHub.DistributedTask.Pipelines.ContextData; using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.Runner.Common.Util; using GitHub.Runner.Common.Util;
using GitHub.Runner.Common; using GitHub.Runner.Common;
@@ -56,5 +56,31 @@ namespace GitHub.Runner.Worker
} }
} }
} }
public double? CheckRunId
{
get
{
if (this.TryGetValue("check_run_id", out var value) && value is NumberContextData number)
{
return number.Value;
}
else
{
return null;
}
}
set
{
if (value.HasValue)
{
this["check_run_id"] = new NumberContextData(value.Value);
}
else
{
this["check_run_id"] = null;
}
}
}
} }
} }

View File

@@ -318,24 +318,17 @@ namespace GitHub.Runner.Worker
{ {
try try
{ {
if (jobContext.Global.Variables.GetBoolean(Constants.Runner.Features.SkipRetryCompleteJobUponKnownErrors) ?? false) await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, default);
{
await runServer.CompleteJob2Async(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, default);
}
else
{
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, default);
}
return result; return result;
} }
catch (VssUnauthorizedException ex) when (jobContext.Global.Variables.GetBoolean(Constants.Runner.Features.SkipRetryCompleteJobUponKnownErrors) ?? false) catch (VssUnauthorizedException ex)
{ {
Trace.Error($"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}."); Trace.Error($"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}.");
Trace.Error(ex); Trace.Error(ex);
exceptions.Add(ex); exceptions.Add(ex);
break; break;
} }
catch (TaskOrchestrationJobNotFoundException ex) when (jobContext.Global.Variables.GetBoolean(Constants.Runner.Features.SkipRetryCompleteJobUponKnownErrors) ?? false) catch (TaskOrchestrationJobNotFoundException ex)
{ {
Trace.Error($"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}."); Trace.Error($"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}.");
Trace.Error(ex); Trace.Error(ex);

View File

@@ -20,7 +20,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" /> <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" /> <PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" /> <PackageReference Include="System.Threading.Channels" Version="8.0.0" />
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" /> <PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
</ItemGroup> </ItemGroup>

View File

@@ -253,11 +253,12 @@ namespace GitHub.Actions.RunService.WebApi
return false; return false;
} }
private static string Truncate(string errorBody) internal static string Truncate(string errorBody)
{ {
if (errorBody.Length > 100) const int maxLength = 200;
if (errorBody.Length > maxLength)
{ {
return errorBody.Substring(0, 100) + "[truncated]"; return errorBody.Substring(0, maxLength) + "[truncated]";
} }
return errorBody; return errorBody;

View File

@@ -14,7 +14,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" /> <PackageReference Include="Azure.Storage.Blobs" Version="12.24.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />

View File

@@ -25,7 +25,10 @@ namespace GitHub.Services.WebApi.Jwt
HS256, HS256,
[EnumMember] [EnumMember]
RS256 RS256,
[EnumMember]
PS256,
} }
//JsonWebToken is marked as DataContract so //JsonWebToken is marked as DataContract so
@@ -286,6 +289,7 @@ namespace GitHub.Services.WebApi.Jwt
{ {
case JWTAlgorithm.HS256: case JWTAlgorithm.HS256:
case JWTAlgorithm.RS256: case JWTAlgorithm.RS256:
case JWTAlgorithm.PS256:
return signingCredentials.SignData(bytes); return signingCredentials.SignData(bytes);
default: default:

View File

@@ -166,6 +166,21 @@ namespace GitHub.Services.WebApi
} }
} }
public override JWTAlgorithm SignatureAlgorithm
{
get
{
if (m_signaturePadding == RSASignaturePadding.Pss)
{
return JWTAlgorithm.PS256;
}
else
{
return base.SignatureAlgorithm;
}
}
}
protected override Byte[] GetSignature(Byte[] input) protected override Byte[] GetSignature(Byte[] input)
{ {
using (var rsa = m_factory()) using (var rsa = m_factory())

View File

@@ -1,10 +1,10 @@
using GitHub.Runner.Common.Util; using System;
using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Xunit; using Xunit;
namespace GitHub.Runner.Common.Tests namespace GitHub.Runner.Common.Tests
@@ -172,6 +172,133 @@ namespace GitHub.Runner.Common.Tests
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void AuthMigrationDisabledByDefault()
{
try
{
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", "100");
// Arrange.
Setup();
// Assert.
Assert.False(_hc.AllowAuthMigration);
// Change migration state is error free.
_hc.EnableAuthMigration("L0Test");
_hc.DeferAuthMigration(TimeSpan.FromHours(1), "L0Test");
}
finally
{
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", null);
// Cleanup.
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task AuthMigrationReenableTaskNotRunningByDefault()
{
try
{
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", "50");
// Arrange.
Setup();
// Assert.
Assert.False(_hc.AllowAuthMigration);
await Task.Delay(TimeSpan.FromMilliseconds(200));
}
finally
{
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", null);
// Cleanup.
Teardown();
}
var logFile = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"trace_{nameof(HostContextL0)}_{nameof(AuthMigrationReenableTaskNotRunningByDefault)}.log");
var logContent = await File.ReadAllTextAsync(logFile);
Assert.Contains("HostContext", logContent);
Assert.DoesNotContain("Auth migration defer timer", logContent);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void AuthMigrationEnableDisable()
{
try
{
// Arrange.
Setup();
var eventFiredCount = 0;
_hc.AuthMigrationChanged += (sender, e) =>
{
eventFiredCount++;
Assert.Equal("L0Test", e.Trace);
};
// Assert.
_hc.EnableAuthMigration("L0Test");
Assert.True(_hc.AllowAuthMigration);
_hc.DeferAuthMigration(TimeSpan.FromHours(1), "L0Test");
Assert.False(_hc.AllowAuthMigration);
Assert.Equal(2, eventFiredCount);
}
finally
{
// Cleanup.
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public async Task AuthMigrationAutoReset()
{
try
{
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", "100");
// Arrange.
Setup();
var eventFiredCount = 0;
_hc.AuthMigrationChanged += (sender, e) =>
{
eventFiredCount++;
Assert.NotEmpty(e.Trace);
};
// Assert.
_hc.EnableAuthMigration("L0Test");
Assert.True(_hc.AllowAuthMigration);
_hc.DeferAuthMigration(TimeSpan.FromMilliseconds(500), "L0Test");
Assert.False(_hc.AllowAuthMigration);
await Task.Delay(TimeSpan.FromSeconds(1));
Assert.True(_hc.AllowAuthMigration);
Assert.Equal(3, eventFiredCount);
}
finally
{
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", null);
// Cleanup.
Teardown();
}
}
private void Setup([CallerMemberName] string testName = "") private void Setup([CallerMemberName] string testName = "")
{ {
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -18,8 +19,6 @@ namespace GitHub.Runner.Common.Tests.Listener
private readonly Mock<IBrokerServer> _brokerServer; private readonly Mock<IBrokerServer> _brokerServer;
private readonly Mock<IRunnerServer> _runnerServer; private readonly Mock<IRunnerServer> _runnerServer;
private readonly Mock<ICredentialManager> _credMgr; private readonly Mock<ICredentialManager> _credMgr;
private Mock<IConfigurationStore> _store;
public BrokerMessageListenerL0() public BrokerMessageListenerL0()
{ {
@@ -27,7 +26,6 @@ namespace GitHub.Runner.Common.Tests.Listener
_config = new Mock<IConfigurationManager>(); _config = new Mock<IConfigurationManager>();
_config.Setup(x => x.LoadSettings()).Returns(_settings); _config.Setup(x => x.LoadSettings()).Returns(_settings);
_credMgr = new Mock<ICredentialManager>(); _credMgr = new Mock<ICredentialManager>();
_store = new Mock<IConfigurationStore>();
_brokerServer = new Mock<IBrokerServer>(); _brokerServer = new Mock<IBrokerServer>();
_runnerServer = new Mock<IRunnerServer>(); _runnerServer = new Mock<IRunnerServer>();
} }
@@ -35,7 +33,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void CreatesSession() public async Task CreatesSession()
{ {
using (TestHostContext tc = CreateTestContext()) using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource()) using (var tokenSource = new CancellationTokenSource())
@@ -50,9 +48,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token)) tokenSource.Token))
.Returns(Task.FromResult(expectedSession)); .Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials()); _credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
// Act. // Act.
BrokerMessageListener listener = new(); BrokerMessageListener listener = new();
@@ -70,12 +66,351 @@ namespace GitHub.Runner.Common.Tests.Listener
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task HandleAuthMigrationChanged()
{
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(true)).Returns(new VssCredentials());
// 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());
tc.EnableAuthMigration("L0Test");
var traceFile = Path.GetTempFileName();
File.Copy(tc.TraceFileName, traceFile, true);
Assert.Contains("Auth migration changed", File.ReadAllText(traceFile));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task CreatesSession_DeferAuthMigration()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
var throwException = true;
var expectedSession = new TaskAgentSession();
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(async (TaskAgentSession session, CancellationToken token) =>
{
await Task.Yield();
if (throwException)
{
throwException = false;
throw new NotSupportedException("Error during create session");
}
return expectedSession;
});
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
// Act.
BrokerMessageListener listener = new();
listener.Initialize(tc);
tc.EnableAuthMigration("L0Test");
Assert.True(tc.AllowAuthMigration);
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.Exactly(2));
_credMgr.Verify(x => x.LoadCredentials(true), Times.Exactly(2));
Assert.False(tc.AllowAuthMigration);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task GetNextMessage()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
var expectedSession = new TaskAgentSession();
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
var expectedMessage = new TaskAgentMessage();
_brokerServer
.Setup(x => x.GetRunnerMessageAsync(
It.IsAny<Guid?>(),
It.IsAny<TaskAgentStatus>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(expectedMessage));
// Act.
BrokerMessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result);
Assert.Equal(CreateSessionResult.Success, result);
TaskAgentMessage message = await listener.GetNextMessageAsync(tokenSource.Token);
trace.Info("message: {0}", message);
// Assert.
Assert.Equal(expectedMessage, message);
_brokerServer
.Verify(x => x.GetRunnerMessageAsync(
It.IsAny<Guid?>(),
It.IsAny<TaskAgentStatus>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()), Times.Once());
_brokerServer.Verify(x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<VssCredentials>()), Times.Once());
_credMgr.Verify(x => x.LoadCredentials(true), Times.Once());
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task GetNextMessage_EnableAuthMigration()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
var expectedSession = new TaskAgentSession();
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
var expectedMessage = new TaskAgentMessage();
_brokerServer
.Setup(x => x.GetRunnerMessageAsync(
It.IsAny<Guid?>(),
It.IsAny<TaskAgentStatus>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(expectedMessage));
// Act.
BrokerMessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result);
Assert.Equal(CreateSessionResult.Success, result);
tc.EnableAuthMigration("L0Test");
TaskAgentMessage message = await listener.GetNextMessageAsync(tokenSource.Token);
trace.Info("message: {0}", message);
// Assert.
Assert.Equal(expectedMessage, message);
_brokerServer
.Verify(x => x.GetRunnerMessageAsync(
It.IsAny<Guid?>(),
It.IsAny<TaskAgentStatus>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()), Times.Once());
_brokerServer.Verify(x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<VssCredentials>()), Times.Exactly(2));
_credMgr.Verify(x => x.LoadCredentials(true), Times.Exactly(2));
Assert.True(tc.AllowAuthMigration);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task GetNextMessage_AuthMigrationFallback()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
tc.EnableAuthMigration("L0Test");
// Arrange.
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
var expectedSession = new TaskAgentSession();
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
var expectedMessage = new TaskAgentMessage();
_brokerServer
.Setup(x => x.GetRunnerMessageAsync(
It.IsAny<Guid?>(),
It.IsAny<TaskAgentStatus>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(async (Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate, CancellationToken token) =>
{
await Task.Yield();
if (tc.AllowAuthMigration)
{
throw new NotSupportedException("Error during get message");
}
return expectedMessage;
});
// Act.
BrokerMessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result);
Assert.Equal(CreateSessionResult.Success, result);
Assert.True(tc.AllowAuthMigration);
TaskAgentMessage message = await listener.GetNextMessageAsync(tokenSource.Token);
trace.Info("message: {0}", message);
// Assert.
Assert.Equal(expectedMessage, message);
_brokerServer
.Verify(x => x.GetRunnerMessageAsync(
It.IsAny<Guid?>(),
It.IsAny<TaskAgentStatus>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()), Times.Exactly(2));
_brokerServer.Verify(x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<VssCredentials>()), Times.Exactly(3));
_credMgr.Verify(x => x.LoadCredentials(true), Times.Exactly(3));
Assert.False(tc.AllowAuthMigration);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task CreatesSessionWithProvidedSettings()
{
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(true)).Returns(new VssCredentials());
// Make sure the config is never called when settings are provided
_config.Setup(x => x.LoadSettings()).Throws(new InvalidOperationException("Should not be called"));
// Act.
// Use the constructor that accepts settings
BrokerMessageListener listener = new(_settings);
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());
// Verify LoadSettings was never called
_config.Verify(x => x.LoadSettings(), Times.Never());
}
}
private TestHostContext CreateTestContext([CallerMemberName] String testName = "") private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
{ {
TestHostContext tc = new(this, testName); TestHostContext tc = new(this, testName);
tc.SetSingleton<IConfigurationManager>(_config.Object); tc.SetSingleton<IConfigurationManager>(_config.Object);
tc.SetSingleton<ICredentialManager>(_credMgr.Object); tc.SetSingleton<ICredentialManager>(_credMgr.Object);
tc.SetSingleton<IConfigurationStore>(_store.Object);
tc.SetSingleton<IBrokerServer>(_brokerServer.Object); tc.SetSingleton<IBrokerServer>(_brokerServer.Object);
tc.SetSingleton<IRunnerServer>(_runnerServer.Object); tc.SetSingleton<IRunnerServer>(_runnerServer.Object);
return tc; return tc;

View File

@@ -1,14 +1,18 @@
using GitHub.Runner.Listener; using System.Collections.Generic;
using System.Security.Cryptography;
using GitHub.Runner.Listener;
using GitHub.Runner.Listener.Configuration; using GitHub.Runner.Listener.Configuration;
using GitHub.Services.Common; using GitHub.Services.Common;
using GitHub.Services.OAuth; using GitHub.Services.OAuth;
using Moq;
using Xunit;
namespace GitHub.Runner.Common.Tests.Listener.Configuration namespace GitHub.Runner.Common.Tests.Listener.Configuration
{ {
public class TestRunnerCredential : CredentialProvider public class TestRunnerCredential : CredentialProvider
{ {
public TestRunnerCredential() : base("TEST") { } public TestRunnerCredential() : base("TEST") { }
public override VssCredentials GetVssCredentials(IHostContext context) public override VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2)
{ {
Tracing trace = context.GetTrace("OuthAccessToken"); Tracing trace = context.GetTrace("OuthAccessToken");
trace.Info("GetVssCredentials()"); trace.Info("GetVssCredentials()");
@@ -23,4 +27,85 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
{ {
} }
} }
}
public class OAuthCredentialTestsL0
{
private Mock<IRSAKeyManager> _rsaKeyManager = new Mock<IRSAKeyManager>();
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "OAuthCredential")]
public void NotUseAuthV2Url()
{
using (TestHostContext hc = new(this))
{
// Arrange.
var oauth = new OAuthCredential();
oauth.CredentialData = new CredentialData()
{
Scheme = Constants.Configuration.OAuth
};
oauth.CredentialData.Data.Add("clientId", "someClientId");
oauth.CredentialData.Data.Add("authorizationUrl", "http://myserver/");
oauth.CredentialData.Data.Add("authorizationUrlV2", "http://myserverv2/");
_rsaKeyManager.Setup(x => x.GetKey()).Returns(RSA.Create(2048));
hc.SetSingleton<IRSAKeyManager>(_rsaKeyManager.Object);
// Act.
var cred = oauth.GetVssCredentials(hc, false); // not allow auth v2
var cred2 = oauth.GetVssCredentials(hc, true); // use auth v2 but hostcontext doesn't
hc.EnableAuthMigration("L0Test");
var cred3 = oauth.GetVssCredentials(hc, false); // not use auth v2 but hostcontext does
oauth.CredentialData.Data.Remove("authorizationUrlV2");
var cred4 = oauth.GetVssCredentials(hc, true); // v2 url is not there
// Assert.
Assert.Equal("http://myserver/", (cred.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
Assert.Equal("someClientId", (cred.Federated as VssOAuthCredential).ClientCredential.ClientId);
Assert.Equal("http://myserver/", (cred2.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
Assert.Equal("someClientId", (cred2.Federated as VssOAuthCredential).ClientCredential.ClientId);
Assert.Equal("http://myserver/", (cred3.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
Assert.Equal("someClientId", (cred3.Federated as VssOAuthCredential).ClientCredential.ClientId);
Assert.Equal("http://myserver/", (cred4.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
Assert.Equal("someClientId", (cred4.Federated as VssOAuthCredential).ClientCredential.ClientId);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "OAuthCredential")]
public void UseAuthV2Url()
{
using (TestHostContext hc = new(this))
{
// Arrange.
var oauth = new OAuthCredential();
oauth.CredentialData = new CredentialData()
{
Scheme = Constants.Configuration.OAuth
};
oauth.CredentialData.Data.Add("clientId", "someClientId");
oauth.CredentialData.Data.Add("authorizationUrl", "http://myserver/");
oauth.CredentialData.Data.Add("authorizationUrlV2", "http://myserverv2/");
_rsaKeyManager.Setup(x => x.GetKey()).Returns(RSA.Create(2048));
hc.SetSingleton<IRSAKeyManager>(_rsaKeyManager.Object);
// Act.
hc.EnableAuthMigration("L0Test");
var cred = oauth.GetVssCredentials(hc, true);
// Assert.
Assert.Equal("http://myserverv2/", (cred.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
Assert.Equal("someClientId", (cred.Federated as VssOAuthCredential).ClientCredential.ClientId);
}
}
}
}

View File

@@ -51,7 +51,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void CreatesSession() public async Task CreatesSession()
{ {
using (TestHostContext tc = CreateTestContext()) using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource()) using (var tokenSource = new CancellationTokenSource())
@@ -67,7 +67,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token)) tokenSource.Token))
.Returns(Task.FromResult(expectedSession)); .Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials()); _credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken }); _store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData)); _store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -95,69 +95,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void CreatesSessionWithBrokerMigration() public async Task DeleteSession()
{
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());
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void DeleteSession()
{ {
using (TestHostContext tc = CreateTestContext()) using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource()) using (var tokenSource = new CancellationTokenSource())
@@ -177,7 +115,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token)) tokenSource.Token))
.Returns(Task.FromResult(expectedSession)); .Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials()); _credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken }); _store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData)); _store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -204,84 +142,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void DeleteSessionWithBrokerMigration() public async Task GetNextMessage()
{
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]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void GetNextMessage()
{ {
using (TestHostContext tc = CreateTestContext()) using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource()) using (var tokenSource = new CancellationTokenSource())
@@ -301,7 +162,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token)) tokenSource.Token))
.Returns(Task.FromResult(expectedSession)); .Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials()); _credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken }); _store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData)); _store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -362,7 +223,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void GetNextMessageWithBrokerMigration() public async Task GetNextMessageWithBrokerMigration()
{ {
using (TestHostContext tc = CreateTestContext()) using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource()) using (var tokenSource = new CancellationTokenSource())
@@ -382,7 +243,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token)) tokenSource.Token))
.Returns(Task.FromResult(expectedSession)); .Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials()); _credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken }); _store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData)); _store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -462,13 +323,22 @@ namespace GitHub.Runner.Common.Tests.Listener
_brokerServer _brokerServer
.Verify(x => x.GetRunnerMessageAsync( .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)); expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length));
_credMgr
.Verify(x => x.LoadCredentials(true), Times.Exactly(brokerMessages.Length));
_brokerServer
.Verify(x => x.UpdateConnectionIfNeeded(brokerMigrationMesage.BrokerBaseUrl, It.IsAny<VssCredentials>()), Times.Exactly(brokerMessages.Length));
_brokerServer
.Verify(x => x.ForceRefreshConnection(It.IsAny<VssCredentials>()), Times.Never);
} }
} }
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void CreateSessionWithOriginalCredential() public async Task CreateSessionWithOriginalCredential()
{ {
using (TestHostContext tc = CreateTestContext()) using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource()) using (var tokenSource = new CancellationTokenSource())
@@ -484,7 +354,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token)) tokenSource.Token))
.Returns(Task.FromResult(expectedSession)); .Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials()); _credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
var originalCred = new CredentialData() { Scheme = Constants.Configuration.OAuth }; var originalCred = new CredentialData() { Scheme = Constants.Configuration.OAuth };
originalCred.Data["authorizationUrl"] = "https://s.server"; originalCred.Data["authorizationUrl"] = "https://s.server";
@@ -513,7 +383,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void SkipDeleteSession_WhenGetNextMessageGetTaskAgentAccessTokenExpiredException() public async Task SkipDeleteSession_WhenGetNextMessageGetTaskAgentAccessTokenExpiredException()
{ {
using (TestHostContext tc = CreateTestContext()) using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource()) using (var tokenSource = new CancellationTokenSource())
@@ -533,7 +403,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token)) tokenSource.Token))
.Returns(Task.FromResult(expectedSession)); .Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials()); _credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken }); _store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData)); _store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -571,5 +441,301 @@ namespace GitHub.Runner.Common.Tests.Listener
_settings.PoolId, expectedSession.SessionId, It.IsAny<CancellationToken>()), Times.Never); _settings.PoolId, expectedSession.SessionId, It.IsAny<CancellationToken>()), Times.Never);
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task HandleAuthMigrationChanged()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
var expectedSession = new TaskAgentSession();
_runnerServer
.Setup(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
// 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.Never());
tc.EnableAuthMigration("L0Test");
var traceFile = Path.GetTempFileName();
File.Copy(tc.TraceFileName, traceFile, true);
Assert.Contains("Auth migration changed", File.ReadAllText(traceFile));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task GetNextMessageWithBrokerMigration_AuthMigrationFallback()
{
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(It.IsAny<bool>())).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);
tc.EnableAuthMigration("L0Test");
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
});
var counter = 0;
_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) =>
{
counter++;
await Task.Yield();
if (counter == 2)
{
throw new NotSupportedException("Something wrong.");
}
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 + 1));
_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 + 1));
_credMgr
.Verify(x => x.LoadCredentials(true), Times.Exactly(brokerMessages.Length + 1));
_brokerServer
.Verify(x => x.UpdateConnectionIfNeeded(brokerMigrationMesage.BrokerBaseUrl, It.IsAny<VssCredentials>()), Times.Exactly(brokerMessages.Length + 1));
_brokerServer
.Verify(x => x.ForceRefreshConnection(It.IsAny<VssCredentials>()), Times.Once());
Assert.False(tc.AllowAuthMigration);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task GetNextMessageWithBrokerMigration_EnableAuthMigration()
{
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(It.IsAny<bool>())).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();
if (!tc.AllowAuthMigration)
{
tc.EnableAuthMigration("L0Test");
}
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));
_credMgr
.Verify(x => x.LoadCredentials(true), Times.Exactly(brokerMessages.Length));
_brokerServer
.Verify(x => x.UpdateConnectionIfNeeded(brokerMigrationMesage.BrokerBaseUrl, It.IsAny<VssCredentials>()), Times.Exactly(brokerMessages.Length));
_brokerServer
.Verify(x => x.ForceRefreshConnection(It.IsAny<VssCredentials>()), Times.Once());
Assert.True(tc.AllowAuthMigration);
}
}
} }
} }

View File

@@ -1,13 +1,13 @@
using System; using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using GitHub.Runner.Listener;
using GitHub.Runner.Common; using GitHub.Runner.Common;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Listener;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using Moq; using Moq;
using Xunit; using Xunit;
using System.Threading;
using GitHub.Runner.Common.Tests;
using System.Text;
namespace GitHub.Runner.Tests.Listener namespace GitHub.Runner.Tests.Listener
{ {
@@ -210,9 +210,9 @@ namespace GitHub.Runner.Tests.Listener
var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(credData))); var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(credData)));
_runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny<int>(), It.Is<string>(s => s == "credentials"), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(encodedConfig); _runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny<int>(), It.Is<string>(s => s == "credentials"), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(encodedConfig);
var _runnerConfigUpdater = new RunnerConfigUpdater(); var _runnerConfigUpdater = new RunnerConfigUpdater();
_runnerConfigUpdater.Initialize(hc); _runnerConfigUpdater.Initialize(hc);
hc.EnableAuthMigration("L0Test");
var validRunnerQualifiedId = "valid/runner/qualifiedid/1"; var validRunnerQualifiedId = "valid/runner/qualifiedid/1";
var configType = "credentials"; var configType = "credentials";
@@ -226,6 +226,7 @@ namespace GitHub.Runner.Tests.Listener
_runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "credentials", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once); _runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "credentials", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("Runner credentials updated successfully")), It.IsAny<CancellationToken>()), Times.Once); _runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("Runner credentials updated successfully")), It.IsAny<CancellationToken>()), Times.Once);
_configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny<CredentialData>()), Times.Once); _configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny<CredentialData>()), Times.Once);
Assert.False(hc.AllowAuthMigration);
} }
} }
@@ -306,7 +307,7 @@ namespace GitHub.Runner.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async Task UpdateRunnerConfigAsync_RefreshRunnerCredetialsFailure_ShouldReportTelemetry() public async Task UpdateRunnerConfigAsync_RefreshRunnerCredentialsFailure_ShouldReportTelemetry()
{ {
using (var hc = new TestHostContext(this)) using (var hc = new TestHostContext(this))
{ {
@@ -510,6 +511,56 @@ namespace GitHub.Runner.Tests.Listener
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task UpdateRunnerConfigAsync_RefreshOAuthCredentialsWithDifferentAuthUrl_ShouldReportTelemetry()
{
using (var hc = new TestHostContext(this))
{
hc.SetSingleton<IConfigurationStore>(_configurationStore.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
// Arrange
var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" };
_configurationStore.Setup(x => x.GetSettings()).Returns(setting);
var credData = new CredentialData
{
Scheme = "OAuth"
};
credData.Data.Add("clientId", "12345");
credData.Data.Add("authorizationUrl", "http://example.com/");
_configurationStore.Setup(x => x.GetCredentials()).Returns(credData);
IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner));
IOUtil.SaveObject(credData, hc.GetConfigFile(WellKnownConfigFile.Credentials));
var differentCredData = new CredentialData
{
Scheme = "OAuth"
};
differentCredData.Data.Add("clientId", "12345");
differentCredData.Data.Add("authorizationUrl", "http://example2.com/");
var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(differentCredData)));
_runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny<int>(), It.Is<string>(s => s == "credentials"), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(encodedConfig);
var _runnerConfigUpdater = new RunnerConfigUpdater();
_runnerConfigUpdater.Initialize(hc);
var validRunnerQualifiedId = "valid/runner/qualifiedid/1";
var configType = "credentials";
var serviceType = "pipelines";
var configRefreshUrl = "http://example.com";
// Act
await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl);
// Assert
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("Credential authorizationUrl in refreshed config")), It.IsAny<CancellationToken>()), Times.Once);
_configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny<CredentialData>()), Times.Never);
}
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
@@ -575,5 +626,53 @@ namespace GitHub.Runner.Tests.Listener
_configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny<RunnerSettings>()), Times.Never); _configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny<RunnerSettings>()), Times.Never);
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task UpdateRunnerConfigAsync_UpdateRunnerCredentials_EnableDisableAuthMigration()
{
using (var hc = new TestHostContext(this))
{
hc.SetSingleton<IConfigurationStore>(_configurationStore.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
// Arrange
var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" };
_configurationStore.Setup(x => x.GetSettings()).Returns(setting);
var credData = new CredentialData
{
Scheme = "OAuth"
};
credData.Data.Add("ClientId", "12345");
credData.Data.Add("AuthorizationUrl", "https://example.com");
credData.Data.Add("AuthorizationUrlV2", "https://example2.com");
_configurationStore.Setup(x => x.GetCredentials()).Returns(credData);
IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner));
IOUtil.SaveObject(credData, hc.GetConfigFile(WellKnownConfigFile.Credentials));
var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(credData)));
_runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny<int>(), It.Is<string>(s => s == "credentials"), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(encodedConfig);
var _runnerConfigUpdater = new RunnerConfigUpdater();
_runnerConfigUpdater.Initialize(hc);
Assert.False(hc.AllowAuthMigration);
var validRunnerQualifiedId = "valid/runner/qualifiedid/1";
var configType = "credentials";
var serviceType = "pipelines";
var configRefreshUrl = "http://example.com";
// Act
await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl);
// Assert
_runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "credentials", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("Runner credentials updated successfully")), It.IsAny<CancellationToken>()), Times.Once);
_configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny<CredentialData>()), Times.Once);
Assert.True(hc.AllowAuthMigration);
}
}
} }
} }

View File

@@ -1,13 +1,15 @@
using GitHub.DistributedTask.WebApi; using System;
using GitHub.Runner.Listener;
using GitHub.Runner.Listener.Configuration;
using Moq;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Listener;
using GitHub.Runner.Listener.Configuration;
using GitHub.Services.Common;
using GitHub.Services.WebApi; using GitHub.Services.WebApi;
using Moq;
using Xunit;
using Pipelines = GitHub.DistributedTask.Pipelines; using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Common.Tests.Listener namespace GitHub.Runner.Common.Tests.Listener
@@ -24,6 +26,9 @@ namespace GitHub.Runner.Common.Tests.Listener
private Mock<IConfigurationStore> _configStore; private Mock<IConfigurationStore> _configStore;
private Mock<ISelfUpdater> _updater; private Mock<ISelfUpdater> _updater;
private Mock<IErrorThrottler> _acquireJobThrottler; private Mock<IErrorThrottler> _acquireJobThrottler;
private Mock<ICredentialManager> _credentialManager;
private Mock<IActionsRunServer> _actionsRunServer;
private Mock<IRunServer> _runServer;
public RunnerL0() public RunnerL0()
{ {
@@ -37,6 +42,9 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore = new Mock<IConfigurationStore>(); _configStore = new Mock<IConfigurationStore>();
_updater = new Mock<ISelfUpdater>(); _updater = new Mock<ISelfUpdater>();
_acquireJobThrottler = new Mock<IErrorThrottler>(); _acquireJobThrottler = new Mock<IErrorThrottler>();
_credentialManager = new Mock<ICredentialManager>();
_actionsRunServer = new Mock<IActionsRunServer>();
_runServer = new Mock<IRunServer>();
} }
private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName) private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName)
@@ -57,7 +65,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
//process 2 new job messages, and one cancel message //process 2 new job messages, and one cancel message
public async void TestRunAsync() public async Task TestRunAsync()
{ {
using (var hc = new TestHostContext(this)) using (var hc = new TestHostContext(this))
{ {
@@ -169,7 +177,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[MemberData(nameof(RunAsServiceTestData))] [MemberData(nameof(RunAsServiceTestData))]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void TestExecuteCommandForRunAsService(string[] args, bool configureAsService, Times expectedTimes) public async Task TestExecuteCommandForRunAsService(string[] args, bool configureAsService, Times expectedTimes)
{ {
using (var hc = new TestHostContext(this)) using (var hc = new TestHostContext(this))
{ {
@@ -177,6 +185,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.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object); hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
var command = new CommandSettings(hc, args); var command = new CommandSettings(hc, args);
@@ -201,7 +210,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void TestMachineProvisionerCLI() public async Task TestMachineProvisionerCLI()
{ {
using (var hc = new TestHostContext(this)) using (var hc = new TestHostContext(this))
{ {
@@ -209,6 +218,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.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object); hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
var command = new CommandSettings(hc, new[] { "run" }); var command = new CommandSettings(hc, new[] { "run" });
@@ -235,7 +245,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void TestRunOnce() public async Task TestRunOnce()
{ {
using (var hc = new TestHostContext(this)) using (var hc = new TestHostContext(this))
{ {
@@ -332,7 +342,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void TestRunOnceOnlyTakeOneJobMessage() public async Task TestRunOnceOnlyTakeOneJobMessage()
{ {
using (var hc = new TestHostContext(this)) using (var hc = new TestHostContext(this))
{ {
@@ -433,7 +443,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void TestRunOnceHandleUpdateMessage() public async Task TestRunOnceHandleUpdateMessage()
{ {
using (var hc = new TestHostContext(this)) using (var hc = new TestHostContext(this))
{ {
@@ -523,13 +533,14 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Runner")] [Trait("Category", "Runner")]
public async void TestRemoveLocalRunnerConfig() public async Task TestRemoveLocalRunnerConfig()
{ {
using (var hc = new TestHostContext(this)) using (var hc = new TestHostContext(this))
{ {
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.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object); hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
var command = new CommandSettings(hc, new[] { "remove", "--local" }); var command = new CommandSettings(hc, new[] { "remove", "--local" });
@@ -549,5 +560,521 @@ namespace GitHub.Runner.Common.Tests.Listener
_configurationManager.Verify(x => x.DeleteLocalRunnerConfig(), Times.Once()); _configurationManager.Verify(x => x.DeleteLocalRunnerConfig(), Times.Once());
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task TestReportAuthMigrationTelemetry()
{
using (var hc = new TestHostContext(this))
{
//Arrange
var runner = new Runner.Listener.Runner();
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
runner.Initialize(hc);
var settings = new RunnerSettings
{
PoolId = 43242,
AgentId = 5678,
Ephemeral = true
};
var message1 = new TaskAgentMessage()
{
MessageId = 4234,
MessageType = "unknown"
};
var messages = new Queue<TaskAgentMessage>();
messages.Enqueue(message1);
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(true));
_configurationManager.Setup(x => x.LoadSettings())
.Returns(settings);
_configurationManager.Setup(x => x.IsConfigured())
.Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
.Returns(async (CancellationToken token) =>
{
hc.GetTrace().Info("Waiting for message");
Assert.False(hc.AllowAuthMigration);
await Task.Delay(100, token);
var traceFile = Path.GetTempFileName();
File.Copy(hc.TraceFileName, traceFile, true);
Assert.DoesNotContain("Checking for auth migration telemetry to report", File.ReadAllText(traceFile));
hc.EnableAuthMigration("L0Test");
hc.DeferAuthMigration(TimeSpan.FromSeconds(1), "L0Test");
hc.EnableAuthMigration("L0Test");
hc.DeferAuthMigration(TimeSpan.FromSeconds(1), "L0Test");
await Task.Delay(1000, token);
hc.ShutdownRunner(ShutdownReason.UserCancelled);
File.Copy(hc.TraceFileName, traceFile, true);
Assert.Contains("Checking for auth migration telemetry to report", File.ReadAllText(traceFile));
return messages.Dequeue();
});
_messageListener.Setup(x => x.DeleteSessionAsync())
.Returns(Task.CompletedTask);
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask);
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
{
});
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(new TaskAgent()));
//Act
var command = new CommandSettings(hc, new string[] { "run" });
var returnCode = await runner.ExecuteCommand(command);
//Assert
Assert.Equal(Constants.Runner.ReturnCode.Success, returnCode);
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeastOnce());
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.Once());
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("L0Test")), It.IsAny<CancellationToken>()), Times.Exactly(4));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task TestRunnerJobRequestMessageFromPipeline()
{
using (var hc = new TestHostContext(this))
{
//Arrange
var runner = new Runner.Listener.Runner();
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<ISelfUpdater>(_updater.Object);
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
hc.EnqueueInstance<IActionsRunServer>(_actionsRunServer.Object);
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
runner.Initialize(hc);
var settings = new RunnerSettings
{
PoolId = 43242,
AgentId = 5678,
Ephemeral = true,
ServerUrl = "https://github.com",
};
var message1 = new TaskAgentMessage()
{
Body = JsonUtility.ToString(new RunnerJobRequestRef() { BillingOwnerId = "github", RunnerRequestId = "999" }),
MessageId = 4234,
MessageType = JobRequestMessageTypes.RunnerJobRequest
};
var messages = new Queue<TaskAgentMessage>();
messages.Enqueue(message1);
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(true));
_configurationManager.Setup(x => x.LoadSettings())
.Returns(settings);
_configurationManager.Setup(x => x.IsConfigured())
.Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
.Returns(async (CancellationToken token) =>
{
if (0 == messages.Count)
{
await Task.Delay(2000, token);
}
return messages.Dequeue();
});
_messageListener.Setup(x => x.DeleteSessionAsync())
.Returns(Task.CompletedTask);
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask);
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
{
});
_actionsRunServer.Setup(x => x.GetJobMessageAsync("999", It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(CreateJobRequestMessage("test")));
_credentialManager.Setup(x => x.LoadCredentials(false)).Returns(new VssCredentials());
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act
var command = new CommandSettings(hc, new string[] { "run" });
Task<int> runnerTask = runner.ExecuteCommand(command);
//Assert
//wait for the runner to exit with right return code
await Task.WhenAny(runnerTask, Task.Delay(30000));
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString());
if (runnerTask.IsCompleted)
{
Assert.Equal(Constants.Runner.ReturnCode.Success, await runnerTask);
}
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once());
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeastOnce());
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.Once());
_credentialManager.Verify(x => x.LoadCredentials(false), Times.Once());
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task TestRunnerJobRequestMessageFromRunService()
{
using (var hc = new TestHostContext(this))
{
//Arrange
var runner = new Runner.Listener.Runner();
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<ISelfUpdater>(_updater.Object);
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
hc.EnqueueInstance<IRunServer>(_runServer.Object);
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
runner.Initialize(hc);
var settings = new RunnerSettings
{
PoolId = 43242,
AgentId = 5678,
Ephemeral = true,
ServerUrl = "https://github.com",
};
var message1 = new TaskAgentMessage()
{
Body = JsonUtility.ToString(new RunnerJobRequestRef() { BillingOwnerId = "github", RunnerRequestId = "999", RunServiceUrl = "https://run-service.com" }),
MessageId = 4234,
MessageType = JobRequestMessageTypes.RunnerJobRequest
};
var messages = new Queue<TaskAgentMessage>();
messages.Enqueue(message1);
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(true));
_configurationManager.Setup(x => x.LoadSettings())
.Returns(settings);
_configurationManager.Setup(x => x.IsConfigured())
.Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
.Returns(async (CancellationToken token) =>
{
if (0 == messages.Count)
{
await Task.Delay(2000, token);
}
return messages.Dequeue();
});
_messageListener.Setup(x => x.DeleteSessionAsync())
.Returns(Task.CompletedTask);
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask);
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
{
});
_runServer.Setup(x => x.GetJobMessageAsync("999", "github", It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(CreateJobRequestMessage("test")));
_credentialManager.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act
var command = new CommandSettings(hc, new string[] { "run" });
Task<int> runnerTask = runner.ExecuteCommand(command);
//Assert
//wait for the runner to exit with right return code
await Task.WhenAny(runnerTask, Task.Delay(30000));
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString());
if (runnerTask.IsCompleted)
{
Assert.Equal(Constants.Runner.ReturnCode.Success, await runnerTask);
}
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once());
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeastOnce());
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.Once());
_credentialManager.Verify(x => x.LoadCredentials(true), Times.Once());
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task TestRunnerJobRequestMessageFromRunService_AuthMigrationFallback()
{
using (var hc = new TestHostContext(this))
{
//Arrange
var runner = new Runner.Listener.Runner();
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<ISelfUpdater>(_updater.Object);
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
hc.EnqueueInstance<IRunServer>(_runServer.Object);
hc.EnqueueInstance<IRunServer>(_runServer.Object);
runner.Initialize(hc);
var settings = new RunnerSettings
{
PoolId = 43242,
AgentId = 5678,
Ephemeral = true,
ServerUrl = "https://github.com",
};
var message1 = new TaskAgentMessage()
{
Body = JsonUtility.ToString(new RunnerJobRequestRef() { BillingOwnerId = "github", RunnerRequestId = "999", RunServiceUrl = "https://run-service.com" }),
MessageId = 4234,
MessageType = JobRequestMessageTypes.RunnerJobRequest
};
var messages = new Queue<TaskAgentMessage>();
messages.Enqueue(message1);
messages.Enqueue(message1);
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(true));
_configurationManager.Setup(x => x.LoadSettings())
.Returns(settings);
_configurationManager.Setup(x => x.IsConfigured())
.Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
.Returns(async (CancellationToken token) =>
{
if (2 == messages.Count)
{
hc.EnableAuthMigration("L0Test");
}
if (0 == messages.Count)
{
await Task.Delay(2000, token);
}
return messages.Dequeue();
});
_messageListener.Setup(x => x.DeleteSessionAsync())
.Returns(Task.CompletedTask);
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask);
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
{
});
var throwError = true;
_runServer.Setup(x => x.GetJobMessageAsync("999", "github", It.IsAny<CancellationToken>()))
.Returns(() =>
{
if (throwError)
{
Assert.True(hc.AllowAuthMigration);
throwError = false;
throw new NotSupportedException("some error");
}
return Task.FromResult(CreateJobRequestMessage("test"));
});
_credentialManager.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act
var command = new CommandSettings(hc, new string[] { "run" });
Task<int> runnerTask = runner.ExecuteCommand(command);
//Assert
//wait for the runner to exit with right return code
await Task.WhenAny(runnerTask, Task.Delay(30000));
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString());
if (runnerTask.IsCompleted)
{
Assert.Equal(Constants.Runner.ReturnCode.Success, await runnerTask);
}
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once());
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeast(2));
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.AtLeast(2));
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
_credentialManager.Verify(x => x.LoadCredentials(true), Times.Exactly(2));
Assert.False(hc.AllowAuthMigration);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task TestRunnerEnableAuthMigrationByDefault()
{
using (var hc = new TestHostContext(this))
{
//Arrange
var runner = new Runner.Listener.Runner();
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
runner.Initialize(hc);
var settings = new RunnerSettings
{
PoolId = 43242,
AgentId = 5678,
Ephemeral = true,
ServerUrl = "https://github.com",
};
var message1 = new TaskAgentMessage()
{
Body = JsonUtility.ToString(new RunnerJobRequestRef() { BillingOwnerId = "github", RunnerRequestId = "999", RunServiceUrl = "https://run-service.com" }),
MessageId = 4234,
MessageType = JobRequestMessageTypes.RunnerJobRequest
};
var messages = new Queue<TaskAgentMessage>();
messages.Enqueue(message1);
messages.Enqueue(message1);
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(true));
_configurationManager.Setup(x => x.LoadSettings())
.Returns(settings);
_configurationManager.Setup(x => x.IsConfigured())
.Returns(true);
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Failure));
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
{
});
var throwError = true;
_runServer.Setup(x => x.GetJobMessageAsync("999", "github", It.IsAny<CancellationToken>()))
.Returns(() =>
{
if (throwError)
{
Assert.True(hc.AllowAuthMigration);
throwError = false;
throw new NotSupportedException("some error");
}
return Task.FromResult(CreateJobRequestMessage("test"));
});
_credentialManager.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var credData = new CredentialData()
{
Scheme = Constants.Configuration.OAuth,
};
credData.Data["ClientId"] = "testClientId";
credData.Data["AuthUrl"] = "https://github.com";
credData.Data["EnableAuthMigrationByDefault"] = "true";
_configStore.Setup(x => x.GetCredentials()).Returns(credData);
Assert.False(hc.AllowAuthMigration);
//Act
var command = new CommandSettings(hc, new string[] { "run" });
var returnCode = await runner.ExecuteCommand(command);
//Assert
Assert.Equal(Constants.Runner.ReturnCode.TerminatedError, returnCode);
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
Assert.True(hc.AllowAuthMigration);
}
}
} }
} }

View File

@@ -0,0 +1,20 @@
using GitHub.Actions.RunService.WebApi;
using Xunit;
namespace GitHub.Actions.RunService.WebApi.Tests;
public sealed class RunServiceHttpClientL0
{
[Fact]
public void Truncate()
{
TestTruncate(string.Empty.PadLeft(199, 'a'), string.Empty.PadLeft(199, 'a'));
TestTruncate(string.Empty.PadLeft(200, 'a'), string.Empty.PadLeft(200, 'a'));
TestTruncate(string.Empty.PadLeft(201, 'a'), string.Empty.PadLeft(200, 'a') + "[truncated]");
}
private void TestTruncate(string errorBody, string expected)
{
Assert.Equal(expected, RunServiceHttpClient.Truncate(errorBody));
}
}

View File

@@ -1,16 +1,15 @@
using GitHub.Runner.Common.Util; using System;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Runtime.Loader;
using System.Reflection;
using System.Collections.Generic;
using GitHub.DistributedTask.Logging; using GitHub.DistributedTask.Logging;
using System.Net.Http.Headers;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common.Tests namespace GitHub.Runner.Common.Tests
@@ -31,6 +30,7 @@ namespace GitHub.Runner.Common.Tests
private StartupType _startupType; private StartupType _startupType;
public event EventHandler Unloading; public event EventHandler Unloading;
public event EventHandler<DelayEventArgs> Delaying; public event EventHandler<DelayEventArgs> Delaying;
public event EventHandler<AuthMigrationEventArgs> AuthMigrationChanged;
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token; public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
public ShutdownReason RunnerShutdownReason { get; private set; } public ShutdownReason RunnerShutdownReason { get; private set; }
public ISecretMasker SecretMasker => _secretMasker; public ISecretMasker SecretMasker => _secretMasker;
@@ -92,6 +92,8 @@ namespace GitHub.Runner.Common.Tests
public RunnerWebProxy WebProxy => new(); public RunnerWebProxy WebProxy => new();
public bool AllowAuthMigration { get; set; }
public async Task Delay(TimeSpan delay, CancellationToken token) public async Task Delay(TimeSpan delay, CancellationToken token)
{ {
// Event callback // Event callback
@@ -101,8 +103,8 @@ namespace GitHub.Runner.Common.Tests
handler(this, new DelayEventArgs(delay, token)); handler(this, new DelayEventArgs(delay, token));
} }
// Delay zero // Delay 10ms
await Task.Delay(TimeSpan.Zero); await Task.Delay(TimeSpan.FromMilliseconds(10));
} }
public T CreateService<T>() where T : class, IRunnerService public T CreateService<T>() where T : class, IRunnerService
@@ -387,6 +389,18 @@ namespace GitHub.Runner.Common.Tests
{ {
return; return;
} }
public void EnableAuthMigration(string trace)
{
AllowAuthMigration = true;
AuthMigrationChanged?.Invoke(this, new AuthMigrationEventArgs(trace));
}
public void DeferAuthMigration(TimeSpan deferred, string trace)
{
AllowAuthMigration = false;
AuthMigrationChanged?.Invoke(this, new AuthMigrationEventArgs(trace));
}
} }
public class DelayEventArgs : EventArgs public class DelayEventArgs : EventArgs

View File

@@ -1168,6 +1168,77 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void InitializeJob_HydratesJobContextWithCheckRunId()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange: Create a job request message and make sure the feature flag is enabled
var variables = new Dictionary<string, VariableValue>()
{
[Constants.Runner.Features.AddCheckRunIdToJobContext] = new VariableValue("true"),
};
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
var pagingLogger = new Moq.Mock<IPagingLogger>();
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger.Object);
hc.SetSingleton(jobServerQueue.Object);
var ec = new Runner.Worker.ExecutionContext();
ec.Initialize(hc);
// Arrange: Add check_run_id to the job context
var jobContext = new Pipelines.ContextData.DictionaryContextData();
jobContext["check_run_id"] = new NumberContextData(123456);
jobRequest.ContextData["job"] = jobContext;
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
// Act
ec.InitializeJob(jobRequest, CancellationToken.None);
// Assert
Assert.NotNull(ec.JobContext);
Assert.Equal(123456, ec.JobContext.CheckRunId);
}
}
// TODO: this test can be deleted when `AddCheckRunIdToJobContext` is fully rolled out
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void InitializeJob_HydratesJobContextWithCheckRunId_FeatureFlagDisabled()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange: Create a job request message and make sure the feature flag is disabled
var variables = new Dictionary<string, VariableValue>()
{
[Constants.Runner.Features.AddCheckRunIdToJobContext] = new VariableValue("false"),
};
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
var pagingLogger = new Moq.Mock<IPagingLogger>();
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger.Object);
hc.SetSingleton(jobServerQueue.Object);
var ec = new Runner.Worker.ExecutionContext();
ec.Initialize(hc);
// Arrange: Add check_run_id to the job context
var jobContext = new Pipelines.ContextData.DictionaryContextData();
jobContext["check_run_id"] = new NumberContextData(123456);
jobRequest.ContextData["job"] = jobContext;
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
// Act
ec.InitializeJob(jobRequest, CancellationToken.None);
// Assert
Assert.NotNull(ec.JobContext);
Assert.Null(ec.JobContext.CheckRunId); // with the feature flag disabled we should not have added a CheckRunId to the JobContext
}
}
private bool ExpressionValuesAssertEqual(DictionaryContextData expect, DictionaryContextData actual) private bool ExpressionValuesAssertEqual(DictionaryContextData expect, DictionaryContextData actual)
{ {
foreach (var key in expect.Keys.ToList()) foreach (var key in expect.Keys.ToList())

View File

@@ -896,5 +896,173 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("not-working", match.Message); Assert.Equal("not-working", match.Message);
Assert.Equal("my-project.proj", match.FromPath); Assert.Equal("my-project.proj", match.FromPath);
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Matcher_SinglePattern_DefaultFromPath()
{
var config = JsonUtility.FromString<IssueMatchersConfig>(@"
{
""problemMatcher"": [
{
""owner"": ""myMatcher"",
""fromPath"": ""subdir/default-project.csproj"",
""pattern"": [
{
""regexp"": ""^file:(.+) line:(.+) column:(.+) severity:(.+) code:(.+) message:(.+)$"",
""file"": 1,
""line"": 2,
""column"": 3,
""severity"": 4,
""code"": 5,
""message"": 6
}
]
}
]
}
");
config.Validate();
var matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
var match = matcher.Match("file:my-file.cs line:123 column:45 severity:real-bad code:uh-oh message:not-working");
Assert.Equal("my-file.cs", match.File);
Assert.Equal("123", match.Line);
Assert.Equal("45", match.Column);
Assert.Equal("real-bad", match.Severity);
Assert.Equal("uh-oh", match.Code);
Assert.Equal("not-working", match.Message);
Assert.Equal("subdir/default-project.csproj", match.FromPath);
// Test that a pattern-specific fromPath overrides the default
config = JsonUtility.FromString<IssueMatchersConfig>(@"
{
""problemMatcher"": [
{
""owner"": ""myMatcher"",
""fromPath"": ""subdir/default-project.csproj"",
""pattern"": [
{
""regexp"": ""^file:(.+) line:(.+) column:(.+) severity:(.+) code:(.+) message:(.+) fromPath:(.+)$"",
""file"": 1,
""line"": 2,
""column"": 3,
""severity"": 4,
""code"": 5,
""message"": 6,
""fromPath"": 7
}
]
}
]
}
");
config.Validate();
matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
match = matcher.Match("file:my-file.cs line:123 column:45 severity:real-bad code:uh-oh message:not-working fromPath:my-project.proj");
Assert.Equal("my-file.cs", match.File);
Assert.Equal("123", match.Line);
Assert.Equal("45", match.Column);
Assert.Equal("real-bad", match.Severity);
Assert.Equal("uh-oh", match.Code);
Assert.Equal("not-working", match.Message);
Assert.Equal("my-project.proj", match.FromPath);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Matcher_MultiplePatterns_DefaultFromPath()
{
var config = JsonUtility.FromString<IssueMatchersConfig>(@"
{
""problemMatcher"": [
{
""owner"": ""myMatcher"",
""fromPath"": ""subdir/default-project.csproj"",
""pattern"": [
{
""regexp"": ""^file:(.+)$"",
""file"": 1,
},
{
""regexp"": ""^severity:(.+)$"",
""severity"": 1
},
{
""regexp"": ""^line:(.+) column:(.+) code:(.+) message:(.+)$"",
""line"": 1,
""column"": 2,
""code"": 3,
""message"": 4
}
]
}
]
}
");
config.Validate();
var matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
var match = matcher.Match("file:my-file.cs");
Assert.Null(match);
match = matcher.Match("severity:real-bad");
Assert.Null(match);
match = matcher.Match("line:123 column:45 code:uh-oh message:not-working");
Assert.Equal("my-file.cs", match.File);
Assert.Equal("123", match.Line);
Assert.Equal("45", match.Column);
Assert.Equal("real-bad", match.Severity);
Assert.Equal("uh-oh", match.Code);
Assert.Equal("not-working", match.Message);
Assert.Equal("subdir/default-project.csproj", match.FromPath);
// Test that pattern-specific fromPath overrides the default
config = JsonUtility.FromString<IssueMatchersConfig>(@"
{
""problemMatcher"": [
{
""owner"": ""myMatcher"",
""fromPath"": ""subdir/default-project.csproj"",
""pattern"": [
{
""regexp"": ""^file:(.+) fromPath:(.+)$"",
""file"": 1,
""fromPath"": 2
},
{
""regexp"": ""^severity:(.+)$"",
""severity"": 1
},
{
""regexp"": ""^line:(.+) column:(.+) code:(.+) message:(.+)$"",
""line"": 1,
""column"": 2,
""code"": 3,
""message"": 4
}
]
}
]
}
");
config.Validate();
matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
match = matcher.Match("file:my-file.cs fromPath:my-project.proj");
Assert.Null(match);
match = matcher.Match("severity:real-bad");
Assert.Null(match);
match = matcher.Match("line:123 column:45 code:uh-oh message:not-working");
Assert.Equal("my-file.cs", match.File);
Assert.Equal("123", match.Line);
Assert.Equal("45", match.Column);
Assert.Equal("real-bad", match.Severity);
Assert.Equal("uh-oh", match.Code);
Assert.Equal("not-working", match.Message);
Assert.Equal("my-project.proj", match.FromPath);
}
} }
} }

View File

@@ -0,0 +1,38 @@
using System;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.Runner.Worker;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public class JobContextL0
{
[Fact]
public void CheckRunId_SetAndGet_WorksCorrectly()
{
var ctx = new JobContext();
ctx.CheckRunId = 12345;
Assert.Equal(12345, ctx.CheckRunId);
Assert.True(ctx.TryGetValue("check_run_id", out var value));
Assert.IsType<NumberContextData>(value);
Assert.Equal(12345, ((NumberContextData)value).Value);
}
[Fact]
public void CheckRunId_NotSet_ReturnsNull()
{
var ctx = new JobContext();
Assert.Null(ctx.CheckRunId);
Assert.False(ctx.TryGetValue("check_run_id", out var value));
}
[Fact]
public void CheckRunId_SetNull_RemovesKey()
{
var ctx = new JobContext();
ctx.CheckRunId = 12345;
ctx.CheckRunId = null;
Assert.Null(ctx.CheckRunId);
}
}
}

View File

@@ -937,6 +937,62 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void MatcherDefaultFromPath()
{
var matchers = new IssueMatchersConfig
{
Matchers =
{
new IssueMatcherConfig
{
Owner = "my-matcher-1",
FromPath = "workflow-repo/some-project/some-project.proj",
Patterns = new[]
{
new IssuePatternConfig
{
Pattern = @"(.+): (.+)",
File = 1,
Message = 2,
},
},
},
},
};
using (var hostContext = Setup(matchers: matchers))
using (_outputManager)
{
// Setup github.workspace, github.repository
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
Directory.CreateDirectory(workDirectory);
var workspaceDirectory = Path.Combine(workDirectory, "workspace");
Directory.CreateDirectory(workspaceDirectory);
_executionContext.Setup(x => x.GetGitHubContext("workspace")).Returns(workspaceDirectory);
_executionContext.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/workflow-repo");
// Setup a git repository
var repositoryPath = Path.Combine(workspaceDirectory, "workflow-repo");
await CreateRepository(hostContext, repositoryPath, "https://github.com/my-org/workflow-repo");
// Create a test file
var filePath = Path.Combine(repositoryPath, "some-project", "some-directory", "some-file.txt");
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
File.WriteAllText(filePath, "");
// Process
Process("some-directory/some-file.txt: some error");
Assert.Equal(1, _issues.Count);
Assert.Equal("some error", _issues[0].Item1.Message);
Assert.Equal("some-project/some-directory/some-file.txt", _issues[0].Item1.Data["file"]);
Assert.Equal(0, _commands.Count);
Assert.Equal(0, _messages.Count);
}
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]

View File

@@ -15,9 +15,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="xunit" Version="2.7.1" /> <PackageReference Include="xunit" Version="2.7.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" /> <PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
<PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" /> <PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" />
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />

View File

@@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout"
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x" DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
PACKAGE_DIR="$SCRIPT_DIR/../_package" PACKAGE_DIR="$SCRIPT_DIR/../_package"
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk" DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
DOTNETSDK_VERSION="8.0.407" DOTNETSDK_VERSION="8.0.408"
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION" DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
RUNNER_VERSION=$(cat runnerversion) RUNNER_VERSION=$(cat runnerversion)

View File

@@ -1,5 +1,5 @@
{ {
"sdk": { "sdk": {
"version": "8.0.407" "version": "8.0.408"
} }
} }

View File

@@ -1 +1 @@
2.323.0 2.324.0