Compare commits

..

89 Commits

Author SHA1 Message Date
Salman Chishti - SalmanMKC
f5d4de2c1e Add InternalsVisibleTo attribute for testing purposes 2025-09-23 01:39:24 +01:00
Salman Chishti - SalmanMKC
2bb77fda53 Fix duplicate using directive in StepHost.cs
Remove duplicate 'using GitHub.Runner.Worker.Container;' statement that was causing compilation error CS0105.
2025-09-23 01:33:09 +01:00
Salman Chishti - SalmanMKC
ece418e8c4 Refactor Docker command options to use escaped options and add tests for quoting environment variables in scripts 2025-09-22 23:28:07 +01:00
Salman Muin Kayser Chishti
2b472844d3 better platform handling 2025-09-22 15:50:50 +01:00
Salman Muin Kayser Chishti
8c6bd3e3c1 simplify logic to not use scripthandler 2025-09-22 15:25:07 +01:00
Salman Muin Kayser Chishti
1ce077fd16 path fix 2025-09-22 15:18:41 +01:00
dependabot[bot]
0b074a3e93 Bump actions/stale from 9 to 10 (#4015)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-19 11:56:15 +01:00
dependabot[bot]
25faeabaa8 Bump actions/github-script from 7.0.1 to 8.0.0 (#4016)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2025-09-19 10:33:20 +00:00
Luke Tomlinson
b121ef832b Use BrokerURL when using RunnerAdmin (#4044) 2025-09-18 14:10:23 +00:00
Salman Chishti
170033c92b feat: add comprehensive dependency monitoring system (#4025) 2025-09-17 16:16:48 +01:00
Salman Chishti
f9c4e17fd9 feat: add comprehensive NPM security management workflow (#4027) 2025-09-11 18:14:50 +00:00
Salman Chishti
646da708ba feat: add automated Node.js version management workflow (#4026) 2025-09-10 20:54:23 +00:00
Salman Chishti
bf8236344b feat: add automated Docker BuildX dependency management workflow (#4029) 2025-09-09 11:40:34 -04:00
Salman Chishti
720f16aef6 feat: add automated .NET dependency management workflow (#4028) 2025-09-09 14:30:56 +01:00
Tingluo Huang
f77066a6a8 Bump node.js to latest version in runner. (#4022) 2025-09-08 16:39:58 +00:00
github-actions[bot]
df83df2a32 Update Docker to v28.4.0 and Buildx to v0.28.0 (#4020)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-07 21:43:15 -04:00
Tingluo Huang
97b2254146 Break UseV2Flow into UseV2Flow and UseRunnerAdminFlow. (#4013) 2025-09-03 17:09:17 -04:00
eric sciple
7f72ba9e48 Map RUNNER_TEMP for container action (#4011) 2025-09-03 11:45:43 -05:00
Salman Chishti
f8ae5bb1a7 chore: migrate Husky config from v8 to v9 format (#4003) 2025-09-01 09:16:05 +00:00
dependabot[bot]
a5631456a2 Bump typescript from 5.2.2 to 5.9.2 in /src/Misc/expressionFunc/hashFiles (#4007)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-01 09:33:54 +01:00
dependabot[bot]
65dfa460ba Bump eslint-plugin-github in /src/Misc/expressionFunc/hashFiles (#3180)
Bumps [eslint-plugin-github](https://github.com/github/eslint-plugin-github) from 4.10.0 to 4.10.2.
- [Release notes](https://github.com/github/eslint-plugin-github/releases)
- [Commits](https://github.com/github/eslint-plugin-github/compare/v4.10.0...v4.10.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-github
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>
2025-08-30 04:03:46 +00:00
dependabot[bot]
80ee51f164 Bump @vercel/ncc from 0.38.0 to 0.38.3 in /src/Misc/expressionFunc/hashFiles (#3841)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2025-08-30 03:24:53 +00:00
dependabot[bot]
c95883f28e Bump husky from 8.0.3 to 9.1.7 in /src/Misc/expressionFunc/hashFiles (#3842)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2025-08-30 03:19:49 +00:00
dependabot[bot]
6e940643a9 Bump @typescript-eslint/eslint-plugin from 6.7.2 to 8.35.0 in /src/Misc/expressionFunc/hashFiles (#3920)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2025-08-29 20:08:31 +00:00
dependabot[bot]
629f2384a4 Bump actions/attest-build-provenance from 2 to 3 (#4002)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 21:01:36 +01:00
github-actions[bot]
c3bf70becb Update dotnet sdk to latest version @8.0.413 (#4000)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-25 02:09:47 +00:00
github-actions[bot]
8b65f5f9df Update Docker to v28.3.3 and Buildx to v0.27.0 (#3999)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-25 00:31:52 +00:00
eric sciple
5f1efec208 Acknowledge runner request (#3996) 2025-08-22 13:52:32 -05:00
Doug Horner
20d82ad357 Update safe_sleep.sh for bug when scheduler is paused for more than 1 second (#3157) 2025-08-20 19:04:48 +00:00
Salman Chishti
0ebdf9e83d Prepare runner release v2.328.0 (#3984) 2025-08-13 17:38:32 +01:00
dependabot[bot]
6543bf206b Bump actions/checkout from 4 to 5 (#3982)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-13 12:44:39 +01:00
dependabot[bot]
a942627965 Bump actions/download-artifact from 4 to 5 (#3973)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 19:32:29 -04:00
dependabot[bot]
83539166c9 Bump Azure.Storage.Blobs from 12.24.0 to 12.25.0 (#3974)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 23:23:54 +00:00
dependabot[bot]
1c1e8bfd18 Bump Microsoft.NET.Test.Sdk from 17.13.0 to 17.14.1 (#3975)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 19:17:20 -04:00
Cory Calahan
59177fa379 Redirect supported OS doc section to current public Docs location (#3979) 2025-08-07 18:49:02 -04:00
djs-intel
2d7635a7f0 Update Node20 and Node24 to latest (#3972) 2025-08-07 22:41:18 +00:00
Salman Chishti
0203cf24d3 Node 20 -> Node 24 migration feature flagging, opt-in and opt-out environment variables (#3948) 2025-08-07 16:30:03 +00:00
Joshua Brooks
5e74a4d8e4 Add V2 flow for runner deletion (#3954) 2025-08-07 10:52:46 -04:00
Salman Chishti
6ca97eeb88 Fix if statement structure in update script and variable reference (#3956) 2025-07-25 16:42:28 +01:00
github-actions[bot]
8a9b96806d Update Docker to v28.3.2 and Buildx to v0.26.1 (#3953)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-23 22:41:02 -04:00
Salman Chishti
dc9cf684c9 Prepare runner release 2.327.0 (#3951) 2025-07-22 18:59:15 +01:00
github-actions[bot]
c765c990b9 Update dotnet sdk to latest version @8.0.412 (#3941)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-22 11:32:03 -04:00
Salman Chishti
ed48ddd08c Runner Support for executing Node24 Actions (#3940) 2025-07-17 01:00:17 +00:00
Salman Chishti
a1e6ad8d2e Fix null reference exception in user agent handling (#3946) 2025-07-17 01:12:03 +01:00
Tingluo Huang
14856e63bc Try add orchestrationid into user-agent using token claim. (#3945) 2025-07-16 14:11:09 -04:00
Tingluo Huang
0d24afa114 Prepare 2.326.0 runner release. (#3936) 2025-07-07 15:19:21 -04:00
Tingluo Huang
20912234a5 Upgrade node.js to latest version. (#3935) 2025-07-07 11:47:48 -04:00
Tingluo Huang
5969cbe208 Bump windows service app to dotnet 4.7 (#3926) 2025-07-01 15:49:30 -04:00
github-actions[bot]
9f57d37642 Update Docker to v28.2.2 and Buildx to v0.25.0 (#3918)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-30 12:43:59 -04:00
github-actions[bot]
60563d82d1 Update dotnet sdk to latest version @8.0.411 (#3911)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-30 11:42:02 -04:00
Ben De St Paer-Gotch
097ada9374 Update README.md (#3898) 2025-06-09 18:16:35 +01:00
Ryan Ghadimi
9b457781d6 runner timestamps invariant (#3888) 2025-06-02 21:47:51 +00:00
Aiqiao Yan
9709b69571 prep for version 2.325.0 (#3889) 2025-06-02 14:40:01 -04:00
Tingluo Huang
acf3f2ba12 Allow NO_SSL_VERIFY in RawHttpMessageHandler. (#3883) 2025-05-30 22:48:16 -04:00
github-actions[bot]
f03fcc8a01 Update Docker to v28.2.1 and Buildx to v0.24.0 (#3881)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-29 18:43:10 -04:00
github-actions[bot]
e4e103c5ed Update dotnet sdk to latest version @8.0.410 (#3871)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-29 17:56:04 -04:00
Aiqiao Yan
a906ec302b show helpful error message when resolving actions directly with launch (#3874) 2025-05-28 22:46:25 -04:00
Tingluo Huang
d9e714496d Allow runner to use authv2 during config. (#3866) 2025-05-21 16:49:35 -04:00
github-actions[bot]
df189ba6e3 Update dotnet sdk to latest version @8.0.409 (#3860)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-19 01:43:01 +00:00
Tingluo Huang
4c1de69e1c Create schedule workflow to upgrade docker and buildx version. (#3859) 2025-05-14 10:04:01 -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
103 changed files with 6442 additions and 891 deletions

View File

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

@@ -41,7 +41,7 @@ jobs:
devScript: ./dev.sh
- runtime: win-x64
os: windows-2019
os: windows-latest
devScript: ./dev
- runtime: win-arm64
@@ -50,7 +50,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Build runner layout
- name: Build & Layout Release

View File

@@ -7,7 +7,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
close-issue-message: "This issue does not seem to be a problem with the runner application, it concerns the GitHub actions platform more generally. Could you please post your feedback on the [GitHub Community Support Forum](https://github.com/orgs/community/discussions/categories/actions) which is actively monitored. Using the forum ensures that we route your problem to the correct team. 😃"
exempt-issue-labels: "keep"

View File

@@ -7,7 +7,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
close-issue-message: "Thank you for your interest in the runner application and taking the time to provide your valuable feedback. We kindly ask you to redirect this feedback to the [GitHub Community Support Forum](https://github.com/orgs/community/discussions/categories/actions-and-packages) which our team actively monitors and would be a better place to start a discussion for new feature requests in GitHub Actions. For more information on this policy please [read our contribution guidelines](https://github.com/actions/runner#contribute). 😃"
exempt-issue-labels: "keep"

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

211
.github/workflows/dependency-check.yml vendored Normal file
View File

@@ -0,0 +1,211 @@
name: Dependency Status Check
on:
workflow_dispatch:
inputs:
check_type:
description: "Type of dependency check"
required: false
default: "all"
type: choice
options:
- all
- node
- dotnet
- docker
- npm
schedule:
- cron: "0 11 * * 1" # Weekly on Monday at 11 AM
jobs:
dependency-status:
runs-on: ubuntu-latest
outputs:
node20-status: ${{ steps.check-versions.outputs.node20-status }}
node24-status: ${{ steps.check-versions.outputs.node24-status }}
dotnet-status: ${{ steps.check-versions.outputs.dotnet-status }}
docker-status: ${{ steps.check-versions.outputs.docker-status }}
buildx-status: ${{ steps.check-versions.outputs.buildx-status }}
npm-vulnerabilities: ${{ steps.check-versions.outputs.npm-vulnerabilities }}
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
steps:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Check dependency versions
id: check-versions
run: |
echo "## Dependency Status Report" >> $GITHUB_STEP_SUMMARY
echo "Generated on: $(date)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Check Node versions
if [[ "${{ github.event.inputs.check_type }}" == "all" || "${{ github.event.inputs.check_type }}" == "node" ]]; then
echo "### Node.js Versions" >> $GITHUB_STEP_SUMMARY
VERSIONS_JSON=$(curl -s https://raw.githubusercontent.com/actions/node-versions/main/versions-manifest.json)
LATEST_NODE20=$(echo "$VERSIONS_JSON" | jq -r '.[] | select(.version | startswith("20.")) | .version' | head -1)
LATEST_NODE24=$(echo "$VERSIONS_JSON" | jq -r '.[] | select(.version | startswith("24.")) | .version' | head -1)
CURRENT_NODE20=$(grep "NODE20_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
CURRENT_NODE24=$(grep "NODE24_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
NODE20_STATUS="✅ up-to-date"
NODE24_STATUS="✅ up-to-date"
if [ "$CURRENT_NODE20" != "$LATEST_NODE20" ]; then
NODE20_STATUS="⚠️ outdated"
fi
if [ "$CURRENT_NODE24" != "$LATEST_NODE24" ]; then
NODE24_STATUS="⚠️ outdated"
fi
echo "| Version | Current | Latest | Status |" >> $GITHUB_STEP_SUMMARY
echo "|---------|---------|--------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Node 20 | $CURRENT_NODE20 | $LATEST_NODE20 | $NODE20_STATUS |" >> $GITHUB_STEP_SUMMARY
echo "| Node 24 | $CURRENT_NODE24 | $LATEST_NODE24 | $NODE24_STATUS |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "node20-status=$NODE20_STATUS" >> $GITHUB_OUTPUT
echo "node24-status=$NODE24_STATUS" >> $GITHUB_OUTPUT
fi
# Check .NET version
if [[ "${{ github.event.inputs.check_type }}" == "all" || "${{ github.event.inputs.check_type }}" == "dotnet" ]]; then
echo "### .NET SDK Version" >> $GITHUB_STEP_SUMMARY
current_dotnet_version=$(jq -r .sdk.version ./src/global.json)
current_major_minor=$(echo "$current_dotnet_version" | cut -d '.' -f 1,2)
latest_dotnet_version=$(curl -sb -H "Accept: application/json" "https://dotnetcli.blob.core.windows.net/dotnet/Sdk/$current_major_minor/latest.version")
DOTNET_STATUS="✅ up-to-date"
if [ "$current_dotnet_version" != "$latest_dotnet_version" ]; then
DOTNET_STATUS="⚠️ outdated"
fi
echo "| Component | Current | Latest | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|---------|--------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| .NET SDK | $current_dotnet_version | $latest_dotnet_version | $DOTNET_STATUS |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "dotnet-status=$DOTNET_STATUS" >> $GITHUB_OUTPUT
fi
# Check Docker versions
if [[ "${{ github.event.inputs.check_type }}" == "all" || "${{ github.event.inputs.check_type }}" == "docker" ]]; then
echo "### Docker Versions" >> $GITHUB_STEP_SUMMARY
current_docker=$(grep "ARG DOCKER_VERSION=" ./images/Dockerfile | cut -d'=' -f2)
current_buildx=$(grep "ARG BUILDX_VERSION=" ./images/Dockerfile | cut -d'=' -f2)
latest_docker=$(curl -s https://download.docker.com/linux/static/stable/x86_64/ | grep -o 'docker-[0-9]*\.[0-9]*\.[0-9]*\.tgz' | sort -V | tail -n 1 | sed 's/docker-\(.*\)\.tgz/\1/')
latest_buildx=$(curl -s https://api.github.com/repos/docker/buildx/releases/latest | jq -r '.tag_name' | sed 's/^v//')
DOCKER_STATUS="✅ up-to-date"
BUILDX_STATUS="✅ up-to-date"
if [ "$current_docker" != "$latest_docker" ]; then
DOCKER_STATUS="⚠️ outdated"
fi
if [ "$current_buildx" != "$latest_buildx" ]; then
BUILDX_STATUS="⚠️ outdated"
fi
echo "| Component | Current | Latest | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|---------|--------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Docker | $current_docker | $latest_docker | $DOCKER_STATUS |" >> $GITHUB_STEP_SUMMARY
echo "| Docker Buildx | $current_buildx | $latest_buildx | $BUILDX_STATUS |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "docker-status=$DOCKER_STATUS" >> $GITHUB_OUTPUT
echo "buildx-status=$BUILDX_STATUS" >> $GITHUB_OUTPUT
fi
# Check npm vulnerabilities
if [[ "${{ github.event.inputs.check_type }}" == "all" || "${{ github.event.inputs.check_type }}" == "npm" ]]; then
echo "### NPM Security Audit" >> $GITHUB_STEP_SUMMARY
cd src/Misc/expressionFunc/hashFiles
npm install --silent
AUDIT_OUTPUT=""
AUDIT_EXIT_CODE=0
# Run npm audit and capture output and exit code
if ! AUDIT_OUTPUT=$(npm audit --json 2>&1); then
AUDIT_EXIT_CODE=$?
fi
# Check if output is valid JSON
if echo "$AUDIT_OUTPUT" | jq . >/dev/null 2>&1; then
VULN_COUNT=$(echo "$AUDIT_OUTPUT" | jq '.metadata.vulnerabilities.total // 0')
# Ensure VULN_COUNT is a number
VULN_COUNT=$(echo "$VULN_COUNT" | grep -o '[0-9]*' | head -1)
VULN_COUNT=${VULN_COUNT:-0}
NPM_STATUS="✅ no vulnerabilities"
if [ "$VULN_COUNT" -gt 0 ] 2>/dev/null; then
NPM_STATUS="⚠️ $VULN_COUNT vulnerabilities found"
# Get vulnerability details
HIGH_VULNS=$(echo "$AUDIT_OUTPUT" | jq '.metadata.vulnerabilities.high // 0')
CRITICAL_VULNS=$(echo "$AUDIT_OUTPUT" | jq '.metadata.vulnerabilities.critical // 0')
echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Critical | $CRITICAL_VULNS |" >> $GITHUB_STEP_SUMMARY
echo "| High | $HIGH_VULNS |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
else
echo "No npm vulnerabilities found ✅" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
else
NPM_STATUS="❌ npm audit failed"
echo "npm audit failed to run or returned invalid JSON ❌" >> $GITHUB_STEP_SUMMARY
echo "Exit code: $AUDIT_EXIT_CODE" >> $GITHUB_STEP_SUMMARY
echo "Output: $AUDIT_OUTPUT" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "npm-vulnerabilities=$NPM_STATUS" >> $GITHUB_OUTPUT
fi
- name: Check for open dependency PRs
id: check-prs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "### Open Dependency PRs" >> $GITHUB_STEP_SUMMARY
# Get open PRs with dependency label
OPEN_PRS=$(gh pr list --label "dependencies" --state open --json number,title,url)
PR_COUNT=$(echo "$OPEN_PRS" | jq '. | length')
if [ "$PR_COUNT" -gt 0 ]; then
echo "Found $PR_COUNT open dependency PR(s):" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "$OPEN_PRS" | jq -r '.[] | "- [#\(.number)](\(.url)) \(.title)"' >> $GITHUB_STEP_SUMMARY
else
echo "No open dependency PRs found ✅" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "open-dependency-prs=$PR_COUNT" >> $GITHUB_OUTPUT
- name: Summary
run: |
echo "### Summary" >> $GITHUB_STEP_SUMMARY
echo "- Check for open PRs with the \`dependency\` label before releases" >> $GITHUB_STEP_SUMMARY
echo "- Review and merge dependency updates regularly" >> $GITHUB_STEP_SUMMARY
echo "- Critical vulnerabilities should be addressed immediately" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Automated workflows run weekly to check for updates:**" >> $GITHUB_STEP_SUMMARY
echo "- Node.js versions (Mondays at 6 AM)" >> $GITHUB_STEP_SUMMARY
echo "- NPM audit fix (Mondays at 7 AM)" >> $GITHUB_STEP_SUMMARY
echo "- .NET SDK updates (Mondays at midnight)" >> $GITHUB_STEP_SUMMARY
echo "- Docker/Buildx updates (Mondays at midnight)" >> $GITHUB_STEP_SUMMARY

View File

@@ -0,0 +1,166 @@
name: "Docker/Buildx Version Upgrade"
on:
schedule:
- cron: "0 0 * * 1" # Run every Monday at midnight
workflow_dispatch: # Allow manual triggering
jobs:
check-versions:
runs-on: ubuntu-latest
outputs:
DOCKER_SHOULD_UPDATE: ${{ steps.check_docker_version.outputs.SHOULD_UPDATE }}
DOCKER_LATEST_VERSION: ${{ steps.check_docker_version.outputs.LATEST_VERSION }}
DOCKER_CURRENT_VERSION: ${{ steps.check_docker_version.outputs.CURRENT_VERSION }}
BUILDX_SHOULD_UPDATE: ${{ steps.check_buildx_version.outputs.SHOULD_UPDATE }}
BUILDX_LATEST_VERSION: ${{ steps.check_buildx_version.outputs.LATEST_VERSION }}
BUILDX_CURRENT_VERSION: ${{ steps.check_buildx_version.outputs.CURRENT_VERSION }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Check Docker version
id: check_docker_version
shell: bash
run: |
# Extract current Docker version from Dockerfile
current_version=$(grep "ARG DOCKER_VERSION=" ./images/Dockerfile | cut -d'=' -f2)
# Fetch latest Docker Engine version from Docker's download site
# This gets the latest Linux static binary version which matches what's used in the Dockerfile
latest_version=$(curl -s https://download.docker.com/linux/static/stable/x86_64/ | grep -o 'docker-[0-9]*\.[0-9]*\.[0-9]*\.tgz' | sort -V | tail -n 1 | sed 's/docker-\(.*\)\.tgz/\1/')
# Extra check to ensure we got a valid version
if [[ ! $latest_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Failed to retrieve a valid Docker version"
exit 1
fi
should_update=0
[ "$current_version" != "$latest_version" ] && should_update=1
echo "CURRENT_VERSION=${current_version}" >> $GITHUB_OUTPUT
echo "LATEST_VERSION=${latest_version}" >> $GITHUB_OUTPUT
echo "SHOULD_UPDATE=${should_update}" >> $GITHUB_OUTPUT
- name: Check Buildx version
id: check_buildx_version
shell: bash
run: |
# Extract current Buildx version from Dockerfile
current_version=$(grep "ARG BUILDX_VERSION=" ./images/Dockerfile | cut -d'=' -f2)
# Fetch latest Buildx version
latest_version=$(curl -s https://api.github.com/repos/docker/buildx/releases/latest | jq -r '.tag_name' | sed 's/^v//')
should_update=0
[ "$current_version" != "$latest_version" ] && should_update=1
echo "CURRENT_VERSION=${current_version}" >> $GITHUB_OUTPUT
echo "LATEST_VERSION=${latest_version}" >> $GITHUB_OUTPUT
echo "SHOULD_UPDATE=${should_update}" >> $GITHUB_OUTPUT
- name: Create annotations for versions
run: |
docker_should_update="${{ steps.check_docker_version.outputs.SHOULD_UPDATE }}"
buildx_should_update="${{ steps.check_buildx_version.outputs.SHOULD_UPDATE }}"
# Show annotation if only Docker needs update
if [[ "$docker_should_update" == "1" && "$buildx_should_update" == "0" ]]; then
echo "::warning ::Docker version (${{ steps.check_docker_version.outputs.LATEST_VERSION }}) needs update but Buildx is current. Only updating when both need updates."
fi
# Show annotation if only Buildx needs update
if [[ "$docker_should_update" == "0" && "$buildx_should_update" == "1" ]]; then
echo "::warning ::Buildx version (${{ steps.check_buildx_version.outputs.LATEST_VERSION }}) needs update but Docker is current. Only updating when both need updates."
fi
# Show annotation when both are current
if [[ "$docker_should_update" == "0" && "$buildx_should_update" == "0" ]]; then
echo "::warning ::Latest Docker version is ${{ steps.check_docker_version.outputs.LATEST_VERSION }} and Buildx version is ${{ steps.check_buildx_version.outputs.LATEST_VERSION }}. No updates needed."
fi
update-versions:
permissions:
pull-requests: write
contents: write
needs: [check-versions]
if: ${{ needs.check-versions.outputs.DOCKER_SHOULD_UPDATE == 1 && needs.check-versions.outputs.BUILDX_SHOULD_UPDATE == 1 }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Update Docker version
shell: bash
run: |
latest_version="${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }}"
current_version="${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }}"
# Update version in Dockerfile
sed -i "s/ARG DOCKER_VERSION=$current_version/ARG DOCKER_VERSION=$latest_version/g" ./images/Dockerfile
- name: Update Buildx version
shell: bash
run: |
latest_version="${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
current_version="${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }}"
# Update version in Dockerfile
sed -i "s/ARG BUILDX_VERSION=$current_version/ARG BUILDX_VERSION=$latest_version/g" ./images/Dockerfile
- name: Commit changes and create Pull Request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Setup branch and commit information
branch_name="feature/docker-buildx-upgrade"
commit_message="Upgrade Docker to v${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Buildx to v${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
pr_title="Update Docker to v${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Buildx to v${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
# Configure git
git config --global user.name "github-actions[bot]"
git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>"
# Create branch or switch to it if it exists
if git show-ref --quiet refs/remotes/origin/$branch_name; then
git fetch origin
git checkout -B "$branch_name" origin/$branch_name
else
git checkout -b "$branch_name"
fi
# Commit and push changes
git commit -a -m "$commit_message"
git push --force origin "$branch_name"
# Create PR body using here-doc for proper formatting
cat > pr_body.txt << 'EOF'
Automated Docker and Buildx version update:
- Docker: ${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }} → ${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }}
- Buildx: ${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }} → ${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}
This update ensures we're using the latest stable Docker and Buildx versions for security and performance improvements.
**Release notes:** https://docs.docker.com/engine/release-notes/
**Next steps:**
- Review the version changes
- Verify container builds work as expected
- Test multi-platform builds if applicable
- Merge when ready
---
Autogenerated by [Docker/Buildx Version Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/docker-buildx-upgrade.yml)
EOF
# Create PR
gh pr create -B main -H "$branch_name" \
--title "$pr_title" \
--label "dependencies" \
--label "dependencies-weekly-check" \
--label "dependencies-not-dependabot" \
--label "docker" \
--body-file pr_body.txt

View File

@@ -2,20 +2,20 @@ name: "DotNet SDK Upgrade"
on:
schedule:
- cron: '0 0 * * 1'
- cron: "0 8 * * 1" # Weekly on Monday at 8 AM UTC (independent of Node.js/NPM)
workflow_dispatch:
jobs:
dotnet-update:
runs-on: ubuntu-latest
outputs:
outputs:
SHOULD_UPDATE: ${{ steps.fetch_latest_version.outputs.SHOULD_UPDATE }}
BRANCH_EXISTS: ${{ steps.fetch_latest_version.outputs.BRANCH_EXISTS }}
DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION: ${{ steps.fetch_latest_version.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Get current major minor version
id: fetch_current_version
shell: bash
@@ -37,7 +37,7 @@ jobs:
# check if git branch already exists for the upgrade
branch_already_exists=0
if git ls-remote --heads --exit-code origin refs/heads/feature/dotnetsdk-upgrade/${latest_patch_version};
then
branch_already_exists=1
@@ -89,17 +89,17 @@ jobs:
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
- name: Create Pull Request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create -B main -H feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }} --title "Update dotnet sdk to latest version @${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}" --body "
https://dotnetcli.blob.core.windows.net/dotnet/Sdk/${{ needs.dotnet-update.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}/latest.version
- uses: actions/checkout@v5
with:
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
- name: Create Pull Request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create -B main -H feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }} --title "Update dotnet sdk to latest version @${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}" --label "dependencies" --label "dependencies-weekly-check" --label "dependencies-not-dependabot" --label "dotnet" --body "
https://dotnetcli.blob.core.windows.net/dotnet/Sdk/${{ needs.dotnet-update.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}/latest.version
---
Autogenerated by [DotNet SDK Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/dotnet-upgrade.yml)"
---
Autogenerated by [DotNet SDK Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/dotnet-upgrade.yml)"

130
.github/workflows/node-upgrade.yml vendored Normal file
View File

@@ -0,0 +1,130 @@
name: Auto Update Node Version
on:
schedule:
- cron: "0 6 * * 1" # Weekly, every Monday
workflow_dispatch:
jobs:
update-node:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Get latest Node versions
id: node-versions
run: |
# Get latest Node.js releases from official GitHub releases
echo "Fetching latest Node.js releases..."
# Get latest v20.x release
LATEST_NODE20=$(curl -s https://api.github.com/repos/nodejs/node/releases | \
jq -r '.[] | select(.tag_name | startswith("v20.")) | .tag_name' | \
head -1 | sed 's/^v//')
# Get latest v24.x release
LATEST_NODE24=$(curl -s https://api.github.com/repos/nodejs/node/releases | \
jq -r '.[] | select(.tag_name | startswith("v24.")) | .tag_name' | \
head -1 | sed 's/^v//')
echo "Found Node.js releases: 20=$LATEST_NODE20, 24=$LATEST_NODE24"
# Verify these versions are available in alpine_nodejs releases
echo "Verifying availability in alpine_nodejs..."
ALPINE_RELEASES=$(curl -s https://api.github.com/repos/actions/alpine_nodejs/releases | jq -r '.[].tag_name')
if ! echo "$ALPINE_RELEASES" | grep -q "^node20-$LATEST_NODE20$"; then
echo "::warning title=Node 20 Fallback::Node 20 version $LATEST_NODE20 not found in alpine_nodejs releases, using fallback"
# Fall back to latest available alpine_nodejs v20 release
LATEST_NODE20=$(echo "$ALPINE_RELEASES" | grep "^node20-" | head -1 | sed 's/^node20-//')
echo "Using latest available alpine_nodejs Node 20: $LATEST_NODE20"
fi
if ! echo "$ALPINE_RELEASES" | grep -q "^node24-$LATEST_NODE24$"; then
echo "::warning title=Node 24 Fallback::Node 24 version $LATEST_NODE24 not found in alpine_nodejs releases, using fallback"
# Fall back to latest available alpine_nodejs v24 release
LATEST_NODE24=$(echo "$ALPINE_RELEASES" | grep "^node24-" | head -1 | sed 's/^node24-//')
echo "Using latest available alpine_nodejs Node 24: $LATEST_NODE24"
fi
echo "latest_node20=$LATEST_NODE20" >> $GITHUB_OUTPUT
echo "latest_node24=$LATEST_NODE24" >> $GITHUB_OUTPUT
# Check current versions in externals.sh
CURRENT_NODE20=$(grep "NODE20_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
CURRENT_NODE24=$(grep "NODE24_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
echo "current_node20=$CURRENT_NODE20" >> $GITHUB_OUTPUT
echo "current_node24=$CURRENT_NODE24" >> $GITHUB_OUTPUT
# Determine if updates are needed
NEEDS_UPDATE20="false"
NEEDS_UPDATE24="false"
if [ "$CURRENT_NODE20" != "$LATEST_NODE20" ]; then
NEEDS_UPDATE20="true"
echo "::notice title=Node 20 Update Available::Current: $CURRENT_NODE20 → Latest: $LATEST_NODE20"
fi
if [ "$CURRENT_NODE24" != "$LATEST_NODE24" ]; then
NEEDS_UPDATE24="true"
echo "::notice title=Node 24 Update Available::Current: $CURRENT_NODE24 → Latest: $LATEST_NODE24"
fi
if [ "$NEEDS_UPDATE20" == "false" ] && [ "$NEEDS_UPDATE24" == "false" ]; then
echo "::notice title=No Updates Needed::All Node.js versions are up to date"
fi
echo "needs_update20=$NEEDS_UPDATE20" >> $GITHUB_OUTPUT
echo "needs_update24=$NEEDS_UPDATE24" >> $GITHUB_OUTPUT
- name: Update externals.sh and create PR
if: steps.node-versions.outputs.needs_update20 == 'true' || steps.node-versions.outputs.needs_update24 == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Update the files
if [ "${{ steps.node-versions.outputs.needs_update20 }}" == "true" ]; then
sed -i 's/NODE20_VERSION="[^"]*"/NODE20_VERSION="${{ steps.node-versions.outputs.latest_node20 }}"/' src/Misc/externals.sh
fi
if [ "${{ steps.node-versions.outputs.needs_update24 }}" == "true" ]; then
sed -i 's/NODE24_VERSION="[^"]*"/NODE24_VERSION="${{ steps.node-versions.outputs.latest_node24 }}"/' src/Misc/externals.sh
fi
# Configure git
git config --global user.name "github-actions[bot]"
git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>"
# Create branch and commit changes
branch_name="chore/update-node"
git checkout -b "$branch_name"
git commit -a -m "chore: update Node versions (20: ${{ steps.node-versions.outputs.latest_node20 }}, 24: ${{ steps.node-versions.outputs.latest_node24 }})"
git push --force origin "$branch_name"
# Create PR body using here-doc for proper formatting
cat > pr_body.txt << 'EOF'
Automated Node.js version update:
- Node 20: ${{ steps.node-versions.outputs.current_node20 }} → ${{ steps.node-versions.outputs.latest_node20 }}
- Node 24: ${{ steps.node-versions.outputs.current_node24 }} → ${{ steps.node-versions.outputs.latest_node24 }}
This update ensures we're using the latest stable Node.js versions for security and performance improvements.
**Note**: When updating Node versions, remember to also create a new release of alpine_nodejs at the updated version following the instructions at: https://github.com/actions/alpine_nodejs
---
Autogenerated by [Node Version Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/node-upgrade.yml)
EOF
# Create PR
gh pr create -B main -H "$branch_name" \
--title "chore: update Node versions" \
--label "dependencies" \
--label "dependencies-weekly-check" \
--label "dependencies-not-dependabot" \
--label "node" \
--label "javascript" \
--body-file pr_body.txt
echo "::notice title=PR Created::Successfully created Node.js version update PR on branch $branch_name"

View File

@@ -0,0 +1,235 @@
name: NPM Audit Fix with TypeScript Auto-Fix
on:
workflow_dispatch:
jobs:
npm-audit-with-ts-fix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: NPM install and audit fix with TypeScript auto-repair
working-directory: src/Misc/expressionFunc/hashFiles
run: |
npm install
# Check for vulnerabilities first
echo "Checking for npm vulnerabilities..."
if npm audit --audit-level=moderate; then
echo "✅ No moderate or higher vulnerabilities found"
exit 0
fi
echo "⚠️ Vulnerabilities found, attempting npm audit fix..."
# Attempt audit fix and capture the result
if npm audit fix; then
echo "✅ npm audit fix completed successfully"
AUDIT_FIX_STATUS="success"
else
echo "⚠️ npm audit fix failed or had issues"
AUDIT_FIX_STATUS="failed"
# Try audit fix with --force as a last resort for critical/high vulns only
echo "Checking if critical/high vulnerabilities remain..."
if ! npm audit --audit-level=high; then
echo "🚨 Critical/high vulnerabilities remain, attempting --force fix..."
if npm audit fix --force; then
echo "⚠️ npm audit fix --force completed (may have breaking changes)"
AUDIT_FIX_STATUS="force-fixed"
else
echo "❌ npm audit fix --force also failed"
AUDIT_FIX_STATUS="force-failed"
fi
else
echo "✅ Only moderate/low vulnerabilities remain after failed fix"
AUDIT_FIX_STATUS="partial-success"
fi
fi
echo "AUDIT_FIX_STATUS=$AUDIT_FIX_STATUS" >> $GITHUB_ENV
# Try to fix TypeScript issues automatically
echo "Attempting to fix TypeScript compatibility issues..."
# Check if build fails
if ! npm run build 2>/dev/null; then
echo "Build failed, attempting automated fixes..."
# Common fix 1: Update @types/node to latest compatible version
echo "Trying to update @types/node to latest version..."
npm update @types/node
# Common fix 2: If that doesn't work, try installing a specific known-good version
if ! npm run build 2>/dev/null; then
echo "Trying specific @types/node version..."
# Try Node 20 compatible version
npm install --save-dev @types/node@^20.0.0
fi
# Common fix 3: Clear node_modules and reinstall if still failing
if ! npm run build 2>/dev/null; then
echo "Clearing node_modules and reinstalling..."
rm -rf node_modules package-lock.json
npm install
# Re-run audit fix after clean install if it was successful before
if [[ "$AUDIT_FIX_STATUS" == "success" || "$AUDIT_FIX_STATUS" == "force-fixed" ]]; then
echo "Re-running npm audit fix after clean install..."
npm audit fix || echo "Audit fix failed on second attempt"
fi
fi
# Common fix 4: Try updating TypeScript itself
if ! npm run build 2>/dev/null; then
echo "Trying to update TypeScript..."
npm update typescript
fi
# Final check
if npm run build 2>/dev/null; then
echo "✅ Successfully fixed TypeScript issues automatically"
else
echo "⚠️ Could not automatically fix TypeScript issues"
fi
else
echo "✅ Build passes after audit fix"
fi
- name: Create PR if changes exist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HUSKY: 0 # Disable husky hooks for automated commits
run: |
# Check if there are any changes
if [ -n "$(git status --porcelain)" ]; then
# Configure git
git config --global user.name "github-actions[bot]"
git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>"
# Create branch and commit changes
branch_name="chore/npm-audit-fix-with-ts-repair"
git checkout -b "$branch_name"
# Commit with --no-verify to skip husky hooks
git commit -a -m "chore: npm audit fix with automated TypeScript compatibility fixes" --no-verify
git push --force origin "$branch_name"
# Check final build status and gather info about what was changed
build_status="✅ Build passes"
fixes_applied=""
cd src/Misc/expressionFunc/hashFiles
# Check what packages were updated
if git diff HEAD~1 package.json | grep -q "@types/node"; then
fixes_applied+="\n- Updated @types/node version for TypeScript compatibility"
fi
if git diff HEAD~1 package.json | grep -q "typescript"; then
fixes_applied+="\n- Updated TypeScript version"
fi
if git diff HEAD~1 package-lock.json | grep -q "resolved"; then
fixes_applied+="\n- Updated package dependencies via npm audit fix"
fi
if ! npm run build 2>/dev/null; then
build_status="⚠️ Build fails - manual review required"
fi
cd - > /dev/null
# Create enhanced PR body using here-doc for proper formatting
audit_status_msg=""
case "$AUDIT_FIX_STATUS" in
"success")
audit_status_msg="✅ **Audit Fix**: Completed successfully"
;;
"partial-success")
audit_status_msg="⚠️ **Audit Fix**: Partial success (only moderate/low vulnerabilities remain)"
;;
"force-fixed")
audit_status_msg="⚠️ **Audit Fix**: Completed with --force (may have breaking changes)"
;;
"failed"|"force-failed")
audit_status_msg="❌ **Audit Fix**: Failed to resolve vulnerabilities"
;;
*)
audit_status_msg="❓ **Audit Fix**: Status unknown"
;;
esac
if [[ "$build_status" == *"fails"* ]]; then
cat > pr_body.txt << EOF
Automated npm audit fix with TypeScript auto-repair for hashFiles dependencies.
**Build Status**: ⚠️ Build fails - manual review required
$audit_status_msg
This workflow attempts to automatically fix TypeScript compatibility issues that may arise from npm audit fixes.
⚠️ **Manual Review Required**: The build is currently failing after automated fixes were attempted.
Common issues and solutions:
- Check for TypeScript version compatibility with Node.js types
- Review breaking changes in updated dependencies
- Consider pinning problematic dependency versions temporarily
- Review tsconfig.json for compatibility settings
**Automated Fix Strategy**:
1. Run npm audit fix with proper error handling
2. Update @types/node to latest compatible version
3. Try Node 20 specific @types/node version if needed
4. Clean reinstall dependencies if conflicts persist
5. Update TypeScript compiler if necessary
---
Autogenerated by [NPM Audit Fix with TypeScript Auto-Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit-ts-fix.yml)
EOF
else
cat > pr_body.txt << EOF
Automated npm audit fix with TypeScript auto-repair for hashFiles dependencies.
**Build Status**: ✅ Build passes
$audit_status_msg
This workflow attempts to automatically fix TypeScript compatibility issues that may arise from npm audit fixes.
✅ **Ready to Merge**: All automated fixes were successful and the build passes.
**Automated Fix Strategy**:
1. Run npm audit fix with proper error handling
2. Update @types/node to latest compatible version
3. Try Node 20 specific @types/node version if needed
4. Clean reinstall dependencies if conflicts persist
5. Update TypeScript compiler if necessary
---
Autogenerated by [NPM Audit Fix with TypeScript Auto-Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit-ts-fix.yml)
EOF
fi
if [ -n "$fixes_applied" ]; then
# Add the fixes applied section to the file
sed -i "/This workflow attempts/a\\
\\
**Automated Fixes Applied**:$fixes_applied" pr_body.txt
fi
# Create PR with appropriate labels
labels="dependencies,dependencies-not-dependabot,typescript,npm,security"
if [[ "$build_status" == *"fails"* ]]; then
labels="dependencies,dependencies-not-dependabot,typescript,npm,security,needs-manual-review"
fi
# Create PR
gh pr create -B main -H "$branch_name" \
--title "chore: npm audit fix with TypeScript auto-repair" \
--label "$labels" \
--body-file pr_body.txt
else
echo "No changes to commit"
fi

137
.github/workflows/npm-audit.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
name: NPM Audit Fix
on:
schedule:
- cron: "0 7 * * 1" # Weekly on Monday at 7 AM UTC
workflow_dispatch:
jobs:
npm-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: NPM install and audit fix
working-directory: src/Misc/expressionFunc/hashFiles
run: |
npm install
# Check what vulnerabilities exist
echo "=== Checking current vulnerabilities ==="
npm audit || true
# Apply audit fix --force to get security updates
echo "=== Applying npm audit fix --force ==="
npm audit fix --force
# Test if build still works and set status
echo "=== Testing build compatibility ==="
if npm run all; then
echo "✅ Build successful after audit fix"
echo "AUDIT_FIX_STATUS=success" >> $GITHUB_ENV
else
echo "❌ Build failed after audit fix - will create PR with fix instructions"
echo "AUDIT_FIX_STATUS=build_failed" >> $GITHUB_ENV
fi
- name: Create PR if changes exist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Check if there are any changes
if [ -n "$(git status --porcelain)" ]; then
# Configure git
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Create branch and commit changes
branch_name="chore/npm-audit-fix-$(date +%Y%m%d)"
git checkout -b "$branch_name"
git add .
git commit -m "chore: npm audit fix for hashFiles dependencies" --no-verify
git push origin "$branch_name"
# Create PR body based on what actually happened
if [ "$AUDIT_FIX_STATUS" = "success" ]; then
cat > pr_body.txt << 'EOF'
Automated npm audit fix for security vulnerabilities in hashFiles dependencies.
**✅ Full Fix Applied Successfully**
This update addresses npm security advisories and ensures dependencies are secure and up-to-date.
**Changes made:**
- Applied `npm audit fix --force` to resolve security vulnerabilities
- Updated package-lock.json with security patches
- Verified build compatibility with `npm run all`
**Next steps:**
- Review the dependency changes
- Verify the hashFiles functionality still works as expected
- Merge when ready
---
Autogenerated by [NPM Audit Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit.yml)
EOF
elif [ "$AUDIT_FIX_STATUS" = "build_failed" ]; then
cat > pr_body.txt << 'EOF'
Automated npm audit fix for security vulnerabilities in hashFiles dependencies.
**⚠️ Security Fixes Applied - Build Issues Need Manual Resolution**
This update applies important security patches but causes build failures that require manual fixes.
**Changes made:**
- Applied `npm audit fix --force` to resolve security vulnerabilities
- Updated package-lock.json with security patches
**⚠️ Build Issues Detected:**
The build fails after applying security fixes, likely due to TypeScript compatibility issues with updated `@types/node`.
**Required Manual Fixes:**
1. Review TypeScript compilation errors in the build output
2. Update TypeScript configuration if needed
3. Consider pinning `@types/node` to a compatible version
4. Run `npm run all` locally to verify fixes
**Next steps:**
- **DO NOT merge until build issues are resolved**
- Apply manual fixes for TypeScript compatibility
- Test the hashFiles functionality still works as expected
- Merge when build passes
---
Autogenerated by [NPM Audit Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit.yml)
EOF
else
# Fallback case
cat > pr_body.txt << 'EOF'
Automated npm audit attempted for security vulnerabilities in hashFiles dependencies.
** No Changes Applied**
No security vulnerabilities were found or no changes were needed.
---
Autogenerated by [NPM Audit Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit.yml)
EOF
fi
# Create PR
gh pr create -B main -H "$branch_name" \
--title "chore: npm audit fix for hashFiles dependencies" \
--label "dependencies" \
--label "dependencies-weekly-check" \
--label "dependencies-not-dependabot" \
--label "npm" \
--label "typescript" \
--label "security" \
--body-file pr_body.txt
else
echo "✅ No changes to commit - npm audit fix did not modify any files"
fi

View File

@@ -11,16 +11,15 @@ jobs:
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Make sure ./releaseVersion match ./src/runnerversion
# Query GitHub release ensure version is not used
- name: Check version
uses: actions/github-script@v7.0.1
uses: actions/github-script@v8.0.0
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const core = require('@actions/core')
const fs = require('fs');
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
const releaseVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '')
@@ -30,7 +29,7 @@ jobs:
return
}
try {
const release = await github.repos.getReleaseByTag({
const release = await github.rest.repos.getReleaseByTag({
owner: '${{ github.event.repository.owner.name }}',
repo: '${{ github.event.repository.name }}',
tag: 'v' + runnerVersion
@@ -78,7 +77,7 @@ jobs:
devScript: ./dev.sh
- runtime: win-x64
os: windows-2019
os: windows-latest
devScript: ./dev
- runtime: win-arm64
@@ -87,7 +86,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Build runner layout
- name: Build & Layout Release
@@ -130,41 +129,41 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Download runner package tar.gz/zip produced by 'build' job
- name: Download Artifact (win-x64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-win-x64
path: ./
- name: Download Artifact (win-arm64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-win-arm64
path: ./
- name: Download Artifact (osx-x64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-osx-x64
path: ./
- name: Download Artifact (osx-arm64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-osx-arm64
path: ./
- name: Download Artifact (linux-x64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-linux-x64
path: ./
- name: Download Artifact (linux-arm)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-linux-arm
path: ./
- name: Download Artifact (linux-arm64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-linux-arm64
path: ./
@@ -172,11 +171,10 @@ jobs:
# Create ReleaseNote file
- name: Create ReleaseNote
id: releaseNote
uses: actions/github-script@v7.0.1
uses: actions/github-script@v8.0.0
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const core = require('@actions/core')
const fs = require('fs');
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)
@@ -216,7 +214,7 @@ jobs:
# Upload release assets (full runner packages)
- name: Upload Release Asset (win-x64)
uses: actions/upload-release-asset@v1.0.1
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -226,7 +224,7 @@ jobs:
asset_content_type: application/octet-stream
- name: Upload Release Asset (win-arm64)
uses: actions/upload-release-asset@v1.0.1
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -236,7 +234,7 @@ jobs:
asset_content_type: application/octet-stream
- name: Upload Release Asset (linux-x64)
uses: actions/upload-release-asset@v1.0.1
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -246,7 +244,7 @@ jobs:
asset_content_type: application/octet-stream
- name: Upload Release Asset (osx-x64)
uses: actions/upload-release-asset@v1.0.1
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -256,7 +254,7 @@ jobs:
asset_content_type: application/octet-stream
- name: Upload Release Asset (osx-arm64)
uses: actions/upload-release-asset@v1.0.1
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -266,7 +264,7 @@ jobs:
asset_content_type: application/octet-stream
- name: Upload Release Asset (linux-arm)
uses: actions/upload-release-asset@v1.0.1
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -276,7 +274,7 @@ jobs:
asset_content_type: application/octet-stream
- name: Upload Release Asset (linux-arm64)
uses: actions/upload-release-asset@v1.0.1
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -298,11 +296,11 @@ jobs:
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Compute image version
id: image
uses: actions/github-script@v7.0.1
uses: actions/github-script@v8.0.0
with:
script: |
const fs = require('fs');
@@ -340,7 +338,7 @@ jobs:
org.opencontainers.image.licenses=MIT
- name: Generate attestation
uses: actions/attest-build-provenance@v2
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}

View File

@@ -7,7 +7,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
stale-issue-message: "This issue is stale because it has been open 365 days with no activity. Remove stale label or comment or this will be closed in 15 days."
close-issue-message: "This issue was closed because it has been stalled for 15 days with no activity."

View File

@@ -1,6 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
cd src/Misc/expressionFunc/hashFiles
npx lint-staged
cd src/Misc/expressionFunc/hashFiles && npx lint-staged

View File

@@ -20,6 +20,20 @@ Runner releases:
![linux](docs/res/linux_sm.png) [Pre-reqs](docs/start/envlinux.md) | [Download](https://github.com/actions/runner/releases)
## Contribute
### Note
We accept contributions in the form of issues and pull requests. The runner typically requires changes across the entire system and we aim for issues in the runner to be entirely self contained and fixable here. Therefore, we will primarily handle bug issues opened in this repo and we kindly request you to create all feature and enhancement requests on the [GitHub Feedback](https://github.com/community/community/discussions/categories/actions-and-packages) page. [Read more about our guidelines here](docs/contribute.md) before contributing.
Thank you for your interest in this GitHub repo, however, right now we are not taking contributions.
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features were working on and what stage theyre in.
We are taking the following steps to better direct requests related to GitHub Actions, including:
1. We will be directing questions and support requests to our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions)
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
3. Security Issues should be handled as per our [security.md](security.md)
We will still provide security updates for this project and fix major breaking changes during this time.
You are welcome to still raise bugs in this repo.

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)
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 @@
Make sure the built-in node.js has access to GitHub.com or GitHub Enterprise Server.
The runner carries its own copy of node.js executable under `<runner_root>/externals/node20/`.
The runner carries its own copies of node.js executables under `<runner_root>/externals/node20/` and `<runner_root>/externals/node24/`.
All javascript base Actions will get executed by the built-in `node` at `<runner_root>/externals/node20/`.
All javascript base Actions will get executed by the built-in `node` at either `<runner_root>/externals/node20/` or `<runner_root>/externals/node24/` depending on the version specified in the action's metadata.
> Not the `node` from `$PATH`

View File

@@ -0,0 +1,217 @@
# Runner Dependency Management Process
## Overview
This document outlines the automated dependency management process for the GitHub Actions Runner, designed to ensure we maintain up-to-date and secure dependencies while providing predictable release cycles.
## Release Schedule
- **Monthly Runner Releases**: New runner versions are released monthly
- **Weekly Dependency Checks**: Automated workflows check for dependency updates every Monday
- **Security Patches**: Critical security vulnerabilities are addressed immediately outside the regular schedule
## Automated Workflows
**Note**: These workflows are implemented across separate PRs for easier review and independent deployment. Each workflow includes comprehensive error handling and security-focused vulnerability detection.
### 1. Foundation Labels
- **Workflow**: `.github/workflows/setup-labels.yml` (PR #4024)
- **Purpose**: Creates consistent dependency labels for all automation workflows
- **Labels**: `dependencies`, `security`, `typescript`, `needs-manual-review`
- **Prerequisite**: Must be merged before other workflows for proper labeling
### 2. Node.js Version Updates
- **Workflow**: `.github/workflows/node-upgrade.yml`
- **Schedule**: Mondays at 6:00 AM UTC
- **Purpose**: Updates Node.js 20 and 24 versions in `src/Misc/externals.sh`
- **Source**: [nodejs.org](https://nodejs.org) and [actions/alpine_nodejs](https://github.com/actions/alpine_nodejs)
- **Priority**: First (NPM depends on current Node.js versions)
### 3. NPM Security Audit
- **Primary Workflow**: `.github/workflows/npm-audit.yml` ("NPM Audit Fix")
- **Schedule**: Mondays at 7:00 AM UTC
- **Purpose**: Automated security vulnerability detection and basic fixes
- **Location**: `src/Misc/expressionFunc/hashFiles/`
- **Features**: npm audit, security patch application, PR creation
- **Dependency**: Runs after Node.js updates for optimal compatibility
- **Fallback Workflow**: `.github/workflows/npm-audit-typescript.yml` ("NPM Audit Fix with TypeScript Auto-Fix")
- **Trigger**: Manual dispatch only
- **Purpose**: Manual security audit with TypeScript compatibility fixes
- **Use Case**: When scheduled workflow fails or needs custom intervention
- **Features**: Enhanced TypeScript auto-repair, graduated security response
- **How to Use**:
1. If the scheduled "NPM Audit Fix" workflow fails, go to Actions tab
2. Select "NPM Audit Fix with TypeScript Auto-Fix" workflow
3. Click "Run workflow" and optionally specify fix level (auto/manual)
4. Review the generated PR for TypeScript compatibility issues
### 4. .NET SDK Updates
- **Workflow**: `.github/workflows/dotnet-upgrade.yml`
- **Schedule**: Mondays at midnight UTC
- **Purpose**: Updates .NET SDK and package versions with build validation
- **Features**: Global.json updates, NuGet package management, compatibility checking
- **Independence**: Runs independently of Node.js/NPM updates
### 5. Docker/Buildx Updates
- **Workflow**: `.github/workflows/docker-buildx-upgrade.yml` ("Docker/Buildx Version Upgrade")
- **Schedule**: Mondays at midnight UTC
- **Purpose**: Updates Docker and Docker Buildx versions with multi-platform validation
- **Features**: Container security scanning, multi-architecture build testing
- **Independence**: Runs independently of other dependency updates
### 6. Dependency Monitoring
- **Workflow**: `.github/workflows/dependency-check.yml` ("Dependency Status Check")
- **Schedule**: Mondays at 11:00 AM UTC
- **Purpose**: Comprehensive status report of all dependencies with security audit
- **Features**: Multi-dependency checking, npm audit status, build validation, choice of specific component checks
- **Summary**: Runs last to capture results from all morning dependency updates
## Release Process Integration
### Pre-Release Checklist
Before each monthly runner release:
1. **Check Dependency PRs**:
```bash
# List all open dependency PRs
gh pr list --label "dependencies" --state open
# List only automated weekly dependency updates
gh pr list --label "dependencies-weekly-check" --state open
# List only custom dependency automation (not dependabot)
gh pr list --label "dependencies-not-dependabot" --state open
```
2. **Run Manual Dependency Check**:
- Go to Actions tab → "Dependency Status Check" → "Run workflow"
- Review the summary for any outdated dependencies
3. **Review and Merge Updates**:
- Prioritize security-related updates
- Test dependency updates in development environment
- Merge approved dependency PRs
### Vulnerability Response
#### Critical Security Vulnerabilities
- **Response Time**: Within 24 hours
- **Process**:
1. Assess impact on runner security
2. Create hotfix branch if runner data security is affected
3. Expedite patch release if necessary
4. Document in security advisory if applicable
#### Non-Critical Vulnerabilities
- **Response Time**: Next monthly release
- **Process**:
1. Evaluate if vulnerability affects runner functionality
2. Include fix in regular dependency update cycle
3. Document in release notes
## Monitoring and Alerts
### GitHub Actions Workflow Status
- All dependency workflows create PRs with the `dependencies` label
- Failed workflows should be investigated immediately
- Weekly dependency status reports are generated automatically
### Manual Checks
You can manually trigger dependency checks:
- **Full Status**: Run "Dependency Status Check" workflow
- **Specific Component**: Use the dropdown to check individual dependencies
## Dependency Labels
All automated dependency PRs are tagged with labels for easy filtering and management:
### Primary Labels
- **`dependencies`**: All automated dependency-related PRs
- **`dependencies-weekly-check`**: Automated weekly dependency updates from scheduled workflows
- **`dependencies-not-dependabot`**: Custom dependency automation (not created by dependabot)
- **`security`**: Security vulnerability fixes and patches
- **`typescript`**: TypeScript compatibility and type definition updates
- **`needs-manual-review`**: Complex updates requiring human verification
### Technology-Specific Labels
- **`node`**: Node.js version updates
- **`javascript`**: JavaScript runtime and tooling updates
- **`npm`**: NPM package and security updates
- **`dotnet`**: .NET SDK and NuGet package updates
- **`docker`**: Docker and container tooling updates
### Workflow-Specific Branches
- **Node.js updates**: `chore/update-node` branch
- **NPM security fixes**: `chore/npm-audit-fix-YYYYMMDD` and `chore/npm-audit-fix-with-ts-repair` branches
- **NuGet/.NET updates**: `feature/dotnetsdk-upgrade/{version}` branches
- **Docker updates**: `feature/docker-buildx-upgrade` branch
## Special Considerations
### Node.js Updates
When updating Node.js versions, remember to:
1. Create a corresponding release in [actions/alpine_nodejs](https://github.com/actions/alpine_nodejs)
2. Follow the alpine_nodejs getting started guide
3. Test container builds with new Node versions
### .NET SDK Updates
- Only patch versions are auto-updated within the same major.minor version
- Major/minor version updates require manual review and testing
### Docker Updates
- Updates include both Docker Engine and Docker Buildx
- Verify compatibility with runner container workflows
## Troubleshooting
### Common Issues
1. **NPM Audit Workflow Fails**:
- Check if `package.json` exists in `src/Misc/expressionFunc/hashFiles/`
- Verify Node.js setup step succeeded
2. **Version Detection Fails**:
- Check if upstream APIs are available
- Verify parsing logic for version extraction
3. **PR Creation Fails**:
- Ensure `GITHUB_TOKEN` has sufficient permissions
- Check if branch already exists
### Contact
For questions about the dependency management process:
- Create an issue with the `dependencies` label
- Review existing dependency management workflows
- Consult the runner team for security-related concerns
## Metrics and KPIs
Track these metrics to measure dependency management effectiveness:
- Number of open dependency PRs at release time
- Time to merge dependency updates
- Number of security vulnerabilities by severity
- Release cycle adherence (monthly target)

View File

@@ -4,7 +4,7 @@
## Supported Distributions and Versions
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#linux)."
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/reference/runners/self-hosted-runners#linux)."
## Install .Net Core 3.x Linux Dependencies

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,20 @@
## What's Changed
* Bump docker/login-action from 2 to 3 by @dependabot in https://github.com/actions/runner/pull/3673
* Bump actions/stale from 8 to 9 by @dependabot in https://github.com/actions/runner/pull/3554
* Bump docker/build-push-action from 3 to 6 by @dependabot in https://github.com/actions/runner/pull/3674
* update node version from 20.18.0 -> 20.18.2 by @aiqiaoy in https://github.com/actions/runner/pull/3682
* Pass BillingOwnerId through Acquire/Complete calls by @luketomlinson in https://github.com/actions/runner/pull/3689
* Do not retry CompleteJobAsync for known non-retryable errors by @ericsciple in https://github.com/actions/runner/pull/3696
* Update dotnet sdk to latest version @8.0.406 by @github-actions in https://github.com/actions/runner/pull/3712
* Update Dockerfile with new docker and buildx versions by @thboop in https://github.com/actions/runner/pull/3680
* chore: remove redundant words by @finaltrip in https://github.com/actions/runner/pull/3705
* fix: actions feedback link is incorrect by @Yaminyam in https://github.com/actions/runner/pull/3165
* Bump actions/github-script from 0.3.0 to 7.0.1 by @dependabot in https://github.com/actions/runner/pull/3557
* Docker container provenance by @paveliak in https://github.com/actions/runner/pull/3736
* Add request-id to http eventsource trace. by @TingluoHuang in https://github.com/actions/runner/pull/3740
* Update Bocker and Buildx version to mitigate images scanners alerts by @Blizter in https://github.com/actions/runner/pull/3750
* Fix typo, add invariant culture to timestamp for workflow log reporting by @GhadimiR in https://github.com/actions/runner/pull/3749
* Create vssconnection to actions service when URL provided. by @TingluoHuang in https://github.com/actions/runner/pull/3751
* Housekeeping: Update npm packages and node version by @thboop in https://github.com/actions/runner/pull/3752
* Improve the out-of-date warning message. by @tecimovic in https://github.com/actions/runner/pull/3595
* Update dotnet sdk to latest version @8.0.407 by @github-actions in https://github.com/actions/runner/pull/3753
* Exit hosted runner cleanly during deprovisioning. by @TingluoHuang in https://github.com/actions/runner/pull/3755
* Send annotation title to run-service. by @TingluoHuang in https://github.com/actions/runner/pull/3757
* Allow server enforce runner settings. by @TingluoHuang in https://github.com/actions/runner/pull/3758
* Support refresh runner configs with pipelines service. by @TingluoHuang in https://github.com/actions/runner/pull/3706
* Update Docker to v28.3.2 and Buildx to v0.26.1 by @github-actions[bot] in https://github.com/actions/runner/pull/3953
* Fix if statement structure in update script and variable reference by @salmanmkc in https://github.com/actions/runner/pull/3956
* Add V2 flow for runner deletion by @Samirat in https://github.com/actions/runner/pull/3954
* Node 20 -> Node 24 migration feature flagging, opt-in and opt-out environment variables by @salmanmkc in https://github.com/actions/runner/pull/3948
* Update Node20 and Node24 to latest by @djs-intel in https://github.com/actions/runner/pull/3972
* Redirect supported OS doc section to current public Docs location by @corycalahan in https://github.com/actions/runner/pull/3979
* Bump Microsoft.NET.Test.Sdk from 17.13.0 to 17.14.1 by @dependabot[bot] in https://github.com/actions/runner/pull/3975
* Bump Azure.Storage.Blobs from 12.24.0 to 12.25.0 by @dependabot[bot] in https://github.com/actions/runner/pull/3974
* Bump actions/download-artifact from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/3973
* Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/3982
## New Contributors
* @finaltrip made their first contribution in https://github.com/actions/runner/pull/3705
* @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
* @Samirat made their first contribution in https://github.com/actions/runner/pull/3954
* @djs-intel made their first contribution in https://github.com/actions/runner/pull/3972
**Full Changelog**: https://github.com/actions/runner/compare/v2.322.0...v2.323.0
**Full Changelog**: https://github.com/actions/runner/compare/v2.327.1...v2.328.0
_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.

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
"lint": "eslint src/**/*.ts",
"pack": "ncc build -o ../../layoutbin/hashFiles",
"all": "npm run format && npm run lint && npm run build && npm run pack",
"prepare": "cd ../../../../ && husky install"
"prepare": "cd ../../../../ && husky"
},
"repository": {
"type": "git",
@@ -36,15 +36,15 @@
},
"devDependencies": {
"@types/node": "^20.6.2",
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.7.2",
"@vercel/ncc": "^0.38.0",
"@vercel/ncc": "^0.38.3",
"eslint": "^8.47.0",
"eslint-plugin-github": "^4.10.0",
"eslint-plugin-github": "^4.10.2",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.3",
"husky": "^9.1.7",
"lint-staged": "^15.5.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2"
"typescript": "^5.9.2"
}
}

View File

@@ -6,7 +6,8 @@ NODE_URL=https://nodejs.org/dist
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
NODE20_VERSION="20.19.0"
NODE20_VERSION="20.19.5"
NODE24_VERSION="24.7.0"
get_abs_path() {
# exploits the fact that pwd will print abs path when no args
@@ -139,6 +140,8 @@ function acquireExternalTool() {
if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
if [[ "$PRECACHE" != "" ]]; then
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
fi
@@ -149,6 +152,8 @@ if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then
# todo: replace these with official release when available
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
if [[ "$PRECACHE" != "" ]]; then
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
fi
@@ -157,21 +162,26 @@ fi
# Download the external tools only for OSX.
if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-x64.tar.gz" node20 fix_nested_dir
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-x64.tar.gz" node24 fix_nested_dir
fi
if [[ "$PACKAGERUNTIME" == "osx-arm64" ]]; then
# node.js v12 doesn't support macOS on arm64.
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-arm64.tar.gz" node20 fix_nested_dir
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-arm64.tar.gz" node24 fix_nested_dir
fi
# Download the external tools for Linux PACKAGERUNTIMEs.
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-x64.tar.gz" node20 fix_nested_dir
acquireExternalTool "$NODE_ALPINE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-alpine-x64.tar.gz" node20_alpine
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-x64.tar.gz" node24 fix_nested_dir
acquireExternalTool "$NODE_ALPINE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-alpine-x64.tar.gz" node24_alpine
fi
if [[ "$PACKAGERUNTIME" == "linux-arm64" ]]; then
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-arm64.tar.gz" node20 fix_nested_dir
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-arm64.tar.gz" node24 fix_nested_dir
fi
if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then

View File

@@ -3299,7 +3299,7 @@ function expand(str, isTop) {
var isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) {
// {a},b}
if (m.post.match(/,.*\}/)) {
if (m.post.match(/,(?!,).*\}/)) {
str = m.pre + '{' + m.body + escClose + m.post;
return expand(str);
}

View File

@@ -123,7 +123,7 @@ fi
# fix upgrade issue with macOS when running as a service
attemptedtargetedfix=0
currentplatform=$(uname | awk '{print tolower($0)}')
if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
if [[ "$currentplatform" == 'darwin' && $restartinteractiverunner -eq 0 ]]; then
# We needed a fix for https://github.com/actions/runner/issues/743
# We will recreate the ./externals/nodeXY/bin/node of the past runner version that launched the runnerlistener service
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
@@ -135,16 +135,22 @@ if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
then
# inspect the open file handles to find the node process
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
nodever="node20"
# Try finding node24 first, then fallback to earlier versions if needed
nodever="node24"
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node20
then
nodever="node16"
nodever="node20"
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node12
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
then
nodever="node12"
nodever="node16"
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node12
then
nodever="node12"
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
fi
fi
fi
if [[ $? -eq 0 && -n "$path" ]]

View File

@@ -1,6 +1,6 @@
#!/bin/bash
SECONDS=0
while [[ $SECONDS != $1 ]]; do
while [[ $SECONDS -lt $1 ]]; do
:
done

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

@@ -23,6 +23,8 @@ namespace GitHub.Runner.Common
Task<TaskAgentMessage> GetRunnerMessageAsync(Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate, CancellationToken token);
Task AcknowledgeRunnerRequestAsync(string runnerRequestId, Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, CancellationToken token);
Task UpdateConnectionIfNeeded(Uri serverUri, VssCredentials credentials);
Task ForceRefreshConnection(VssCredentials credentials);
@@ -37,6 +39,7 @@ namespace GitHub.Runner.Common
public async Task ConnectAsync(Uri serverUri, VssCredentials credentials)
{
Trace.Entering();
_brokerUri = serverUri;
_connection = VssUtil.CreateRawConnection(serverUri, credentials);
@@ -66,10 +69,17 @@ namespace GitHub.Runner.Common
var brokerSession = RetryRequest<TaskAgentMessage>(
async () => await _brokerHttpClient.GetRunnerMessageAsync(sessionId, version, status, os, architecture, disableUpdate, cancellationToken), cancellationToken, shouldRetry: ShouldRetryException);
return brokerSession;
}
public async Task AcknowledgeRunnerRequestAsync(string runnerRequestId, Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, CancellationToken cancellationToken)
{
CheckConnection();
// No retries
await _brokerHttpClient.AcknowledgeRunnerRequestAsync(runnerRequestId, sessionId, version, status, os, architecture, cancellationToken);
}
public async Task DeleteSessionAsync(CancellationToken cancellationToken)
{
CheckConnection();
@@ -88,7 +98,12 @@ namespace GitHub.Runner.Common
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)

View File

@@ -53,6 +53,9 @@ namespace GitHub.Runner.Common
[DataMember(EmitDefaultValue = false)]
public bool UseV2Flow { get; set; }
[DataMember(EmitDefaultValue = false)]
public bool UseRunnerAdminFlow { get; set; }
[DataMember(EmitDefaultValue = false)]
public string ServerUrlV2 { get; set; }
@@ -116,6 +119,7 @@ namespace GitHub.Runner.Common
bool IsConfigured();
bool IsServiceConfigured();
bool HasCredentials();
bool IsMigratedConfigured();
CredentialData GetCredentials();
CredentialData GetMigratedCredentials();
RunnerSettings GetSettings();
@@ -198,6 +202,14 @@ namespace GitHub.Runner.Common
return serviceConfigured;
}
public bool IsMigratedConfigured()
{
Trace.Info("IsMigratedConfigured()");
bool configured = new FileInfo(_migratedConfigFilePath).Exists;
Trace.Info("IsMigratedConfigured: {0}", configured);
return configured;
}
public CredentialData GetCredentials()
{
if (_creds == null)

View File

@@ -155,15 +155,37 @@ namespace GitHub.Runner.Common
public const int RunnerUpdating = 3;
public const int RunOnceRunnerUpdating = 4;
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 readonly string DiskSpaceWarning = "runner.diskspace.warning";
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 AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors";
public static readonly string ContainerActionRunnerTemp = "actions_container_action_runner_temp";
}
// Node version migration related constants
public static class NodeMigration
{
// Node versions
public static readonly string Node20 = "node20";
public static readonly string Node24 = "node24";
// Environment variables for controlling node version selection
public static readonly string ForceNode24Variable = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24";
public static readonly string AllowUnsecureNodeVersionVariable = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
// Feature flags for controlling the migration phases
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";
}
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";

View File

@@ -15,6 +15,7 @@ using System.Threading.Tasks;
using GitHub.DistributedTask.Logging;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Services.WebApi.Jwt;
namespace GitHub.Runner.Common
{
@@ -37,6 +38,11 @@ namespace GitHub.Runner.Common
void ShutdownRunner(ShutdownReason reason);
void WritePerfCounter(string counter);
void LoadDefaultUserAgents();
bool AllowAuthMigration { get; }
void EnableAuthMigration(string trace);
void DeferAuthMigration(TimeSpan deferred, string trace);
event EventHandler<AuthMigrationEventArgs> AuthMigrationChanged;
}
public enum StartupType
@@ -70,12 +76,21 @@ namespace GitHub.Runner.Common
private RunnerWebProxy _webProxy = new();
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<AuthMigrationEventArgs> AuthMigrationChanged;
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
public ShutdownReason RunnerShutdownReason { get; private set; }
public ISecretMasker SecretMasker => _secretMasker;
public List<ProductInfoHeaderValue> UserAgents => _userAgents;
public RunnerWebProxy WebProxy => _webProxy;
public bool AllowAuthMigration => _allowAuthMigration.IsSet;
public HostContext(string hostType, string logFile = null)
{
// Validate args.
@@ -207,6 +222,71 @@ namespace GitHub.Runner.Common
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()
{
if (string.IsNullOrEmpty(WebProxy.HttpProxyAddress) && string.IsNullOrEmpty(WebProxy.HttpsProxyAddress))
@@ -227,6 +307,36 @@ namespace GitHub.Runner.Common
{
_userAgents.Add(new ProductInfoHeaderValue("ClientId", clientId));
}
// for Hosted runner, we can pull orchestrationId from JWT claims of the runner listening token.
if (credData != null &&
credData.Scheme == Constants.Configuration.OAuthAccessToken &&
credData.Data.TryGetValue(Constants.Runner.CommandLine.Args.Token, out var accessToken) &&
!string.IsNullOrEmpty(accessToken))
{
try
{
var jwt = JsonWebToken.Create(accessToken);
var claims = jwt.ExtractClaims();
var orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orch_id", StringComparison.OrdinalIgnoreCase))?.Value;
if (string.IsNullOrEmpty(orchestrationId))
{
// fallback to orchid for C# actions-service
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
}
if (!string.IsNullOrEmpty(orchestrationId))
{
_trace.Info($"Pull OrchestrationId {orchestrationId} from runner JWT claims");
_userAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", orchestrationId));
}
}
catch (Exception ex)
{
_trace.Error("Fail to extract OrchestrationId from runner JWT claims");
_trace.Error(ex);
}
}
}
var runnerFile = GetConfigFile(WellKnownConfigFile.Runner);
@@ -549,6 +659,18 @@ namespace GitHub.Runner.Common
_loadContext.Unloading -= LoadContext_Unloading;
_loadContext = null;
}
if (_authMigrationAutoReenableTask != null)
{
_authMigrationAutoReenableTaskCancellationTokenSource?.Cancel();
}
if (_authMigrationAutoReenableTaskCancellationTokenSource != null)
{
_authMigrationAutoReenableTaskCancellationTokenSource?.Dispose();
_authMigrationAutoReenableTaskCancellationTokenSource = null;
}
_httpTraceSubscription?.Dispose();
_diagListenerSubscription?.Dispose();
_traceManager?.Dispose();

View File

@@ -15,7 +15,7 @@ namespace GitHub.Runner.Common
{
void InitializeLaunchClient(Uri uri, string token);
Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken);
Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors);
}
public sealed class LaunchServer : RunnerService, ILaunchServer
@@ -42,12 +42,16 @@ namespace GitHub.Runner.Common
}
public Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList,
CancellationToken cancellationToken)
CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors)
{
if (_launchClient != null)
{
return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList,
cancellationToken: cancellationToken);
if (!displayHelpfulActionsDownloadErrors)
{
return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList,
cancellationToken: cancellationToken);
}
return _launchClient.GetResolveActionsDownloadInfoAsyncV2(planId, jobId, actionReferenceList, cancellationToken);
}
throw new InvalidOperationException("Launch client is not initialized.");

View File

@@ -32,18 +32,6 @@ namespace GitHub.Runner.Common
string billingOwnerId,
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);
}
@@ -82,7 +70,6 @@ namespace GitHub.Runner.Common
ex is not TaskOrchestrationJobUnprocessableException); // HTTP status 422
}
// Legacy will be deleted when SkipRetryCompleteJobUponKnownErrors is cleaned up
public Task CompleteJobAsync(
Guid planId,
Guid jobId,
@@ -94,23 +81,6 @@ namespace GitHub.Runner.Common
IList<Telemetry> telemetry,
string billingOwnerId,
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();
return RetryRequest(
@@ -124,7 +94,9 @@ namespace GitHub.Runner.Common
{
CheckConnection();
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

@@ -19,6 +19,7 @@ namespace GitHub.Runner.Common
Task<DistributedTask.WebApi.Runner> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey);
Task<DistributedTask.WebApi.Runner> ReplaceRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey);
Task DeleteRunnerAsync(string githubUrl, string githubToken, ulong runnerId);
Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken);
}
@@ -43,117 +44,15 @@ namespace GitHub.Runner.Common
public async Task<List<TaskAgent>> GetRunnerByNameAsync(string githubUrl, string githubToken, string agentName)
{
var githubApiUrl = "";
var gitHubUrlBuilder = new UriBuilder(githubUrl);
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
var isOrgRunner = path.Length == 1;
var isRepoOrEnterpriseRunner = path.Length == 2;
var isRepoRunner = isRepoOrEnterpriseRunner && !string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase);
if (isOrgRunner)
{
// org runner
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
}
}
else if (isRepoOrEnterpriseRunner)
{
// Repository runner
if (isRepoRunner)
{
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/repos/{path[0]}/{path[1]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/repos/{path[0]}/{path[1]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
}
}
else
{
// Enterprise runner
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
}
}
}
else
{
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
}
var githubApiUrl = $"{GetEntityUrl(githubUrl)}/runners?name={Uri.EscapeDataString(agentName)}";
var runnersList = await RetryRequest<ListRunnersResponse>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
return runnersList.ToTaskAgents();
}
public async Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken)
{
var githubApiUrl = "";
var gitHubUrlBuilder = new UriBuilder(githubUrl);
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
var isOrgRunner = path.Length == 1;
var isRepoOrEnterpriseRunner = path.Length == 2;
var isRepoRunner = isRepoOrEnterpriseRunner && !string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase);
if (isOrgRunner)
{
// org runner
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runner-groups";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runner-groups";
}
}
else if (isRepoOrEnterpriseRunner)
{
// Repository Runner
if (isRepoRunner)
{
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/repos/{path[0]}/{path[1]}/actions/runner-groups";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/repos/{path[0]}/{path[1]}/actions/runner-groups";
}
}
else
{
// Enterprise Runner
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions/runner-groups";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions/runner-groups";
}
}
}
else
{
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
}
var githubApiUrl = $"{GetEntityUrl(githubUrl)}/runner-groups";
var agentPools = await RetryRequest<RunnerGroupList>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
return agentPools?.ToAgentPoolList();
}
@@ -204,6 +103,12 @@ namespace GitHub.Runner.Common
return await RetryRequest<DistributedTask.WebApi.Runner>(githubApiUrl, githubToken, RequestType.Post, 3, "Failed to add agent", body);
}
public async Task DeleteRunnerAsync(string githubUrl, string githubToken, ulong runnerId)
{
var githubApiUrl = $"{GetEntityUrl(githubUrl)}/runners/{runnerId}";
await RetryRequest<DistributedTask.WebApi.Runner>(githubApiUrl, githubToken, RequestType.Delete, 3, "Failed to delete agent");
}
private async Task<T> RetryRequest<T>(string githubApiUrl, string githubToken, RequestType requestType, int maxRetryAttemptsCount = 5, string errorMessage = null, StringContent body = null)
{
int retry = 0;
@@ -220,13 +125,22 @@ namespace GitHub.Runner.Common
try
{
HttpResponseMessage response = null;
if (requestType == RequestType.Get)
switch (requestType)
{
response = await httpClient.GetAsync(githubApiUrl);
}
else
{
response = await httpClient.PostAsync(githubApiUrl, body);
case RequestType.Get:
response = await httpClient.GetAsync(githubApiUrl);
break;
case RequestType.Post:
response = await httpClient.PostAsync(githubApiUrl, body);
break;
case RequestType.Patch:
response = await httpClient.PatchAsync(githubApiUrl, body);
break;
case RequestType.Delete:
response = await httpClient.DeleteAsync(githubApiUrl);
break;
default:
throw new ArgumentOutOfRangeException(nameof(requestType), requestType, null);
}
if (response != null)
@@ -261,5 +175,61 @@ namespace GitHub.Runner.Common
await Task.Delay(backOff);
}
}
private string GetEntityUrl(string githubUrl)
{
var githubApiUrl = "";
var gitHubUrlBuilder = new UriBuilder(githubUrl);
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
var isOrgRunner = path.Length == 1;
var isRepoOrEnterpriseRunner = path.Length == 2;
var isRepoRunner = isRepoOrEnterpriseRunner && !string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase);
if (isOrgRunner)
{
// org runner
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions";
}
}
else if (isRepoOrEnterpriseRunner)
{
// Repository Runner
if (isRepoRunner)
{
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/repos/{path[0]}/{path[1]}/actions";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/repos/{path[0]}/{path[1]}/actions";
}
}
else
{
// Enterprise Runner
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions";
}
else
{
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions";
}
}
}
else
{
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
}
return githubApiUrl;
}
}
}

View File

@@ -70,7 +70,7 @@ namespace GitHub.Runner.Common
protected async Task RetryRequest(Func<Task> func,
CancellationToken cancellationToken,
int maxRetryAttemptsCount = 5,
int maxAttempts = 5,
Func<Exception, bool> shouldRetry = null
)
{
@@ -79,31 +79,31 @@ namespace GitHub.Runner.Common
await func();
return Unit.Value;
}
await RetryRequest<Unit>(wrappedFunc, cancellationToken, maxRetryAttemptsCount, shouldRetry);
await RetryRequest<Unit>(wrappedFunc, cancellationToken, maxAttempts, shouldRetry);
}
protected async Task<T> RetryRequest<T>(Func<Task<T>> func,
CancellationToken cancellationToken,
int maxRetryAttemptsCount = 5,
int maxAttempts = 5,
Func<Exception, bool> shouldRetry = null
)
{
var retryCount = 0;
var attempt = 0;
while (true)
{
retryCount++;
attempt++;
cancellationToken.ThrowIfCancellationRequested();
try
{
return await func();
}
// TODO: Add handling of non-retriable exceptions: https://github.com/github/actions-broker/issues/122
catch (Exception ex) when (retryCount < maxRetryAttemptsCount && (shouldRetry == null || shouldRetry(ex)))
catch (Exception ex) when (attempt < maxAttempts && (shouldRetry == null || shouldRetry(ex)))
{
Trace.Error("Catch exception during request");
Trace.Error(ex);
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
Trace.Warning($"Back off {backOff.TotalSeconds} seconds before next retry. {maxRetryAttemptsCount - retryCount} attempt left.");
Trace.Warning($"Back off {backOff.TotalSeconds} seconds before next retry. {maxAttempts - attempt} attempt left.");
await Task.Delay(backOff, cancellationToken);
}
}

View File

@@ -1,10 +1,33 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common.Util
{
public static class NodeUtil
{
/// <summary>
/// Represents details about an environment variable, including its value and source
/// </summary>
private class EnvironmentVariableInfo
{
/// <summary>
/// Gets or sets whether the value evaluates to true
/// </summary>
public bool IsTrue { get; set; }
/// <summary>
/// Gets or sets whether the value came from the workflow environment
/// </summary>
public bool FromWorkflow { get; set; }
/// <summary>
/// Gets or sets whether the value came from the system environment
/// </summary>
public bool FromSystem { get; set; }
}
private const string _defaultNodeVersion = "node20";
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] { "node20" });
public static string GetInternalNodeVersion()
@@ -18,5 +41,122 @@ namespace GitHub.Runner.Common.Util
}
return _defaultNodeVersion;
}
/// <summary>
/// Determines the appropriate Node version for Actions to use
/// </summary>
/// <param name="workflowEnvironment">Optional dictionary containing workflow-level environment variables</param>
/// <param name="useNode24ByDefault">Feature flag indicating if Node 24 should be the default</param>
/// <param name="requireNode24">Feature flag indicating if Node 24 is required</param>
/// <returns>The Node version to use (node20 or node24) and warning message if both env vars are set</returns>
public static (string nodeVersion, string warningMessage) DetermineActionsNodeVersion(
IDictionary<string, string> workflowEnvironment = null,
bool useNode24ByDefault = false,
bool requireNode24 = false)
{
// Phase 3: Always use Node 24 regardless of environment variables
if (requireNode24)
{
return (Constants.Runner.NodeMigration.Node24, null);
}
// Get environment variable details with source information
var forceNode24Details = GetEnvironmentVariableDetails(
Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment);
var allowUnsecureNodeDetails = GetEnvironmentVariableDetails(
Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, workflowEnvironment);
bool forceNode24 = forceNode24Details.IsTrue;
bool allowUnsecureNode = allowUnsecureNodeDetails.IsTrue;
string warningMessage = null;
// Check if both flags are set from the same source
bool bothFromWorkflow = forceNode24Details.IsTrue && allowUnsecureNodeDetails.IsTrue &&
forceNode24Details.FromWorkflow && allowUnsecureNodeDetails.FromWorkflow;
bool bothFromSystem = forceNode24Details.IsTrue && allowUnsecureNodeDetails.IsTrue &&
forceNode24Details.FromSystem && allowUnsecureNodeDetails.FromSystem;
// Handle the case when both are set in the same source
if (bothFromWorkflow || bothFromSystem)
{
string source = bothFromWorkflow ? "workflow" : "system";
string defaultVersion = useNode24ByDefault ? Constants.Runner.NodeMigration.Node24 : Constants.Runner.NodeMigration.Node20;
warningMessage = $"Both {Constants.Runner.NodeMigration.ForceNode24Variable} and {Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable} environment variables are set to true in the {source} environment. This is likely a configuration error. Using the default Node version: {defaultVersion}.";
return (defaultVersion, warningMessage);
}
// Phase 2: Node 24 is the default
if (useNode24ByDefault)
{
if (allowUnsecureNode)
{
return (Constants.Runner.NodeMigration.Node20, null);
}
return (Constants.Runner.NodeMigration.Node24, null);
}
// Phase 1: Node 20 is the default
if (forceNode24)
{
return (Constants.Runner.NodeMigration.Node24, null);
}
return (Constants.Runner.NodeMigration.Node20, null);
}
/// <summary>
/// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed.
/// </summary>
/// <param name="preferredVersion">The preferred Node version</param>
/// <returns>A tuple containing the adjusted node version and an optional warning message</returns>
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
{
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) &&
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
{
return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
}
return (preferredVersion, null);
}
/// <summary>
/// Gets detailed information about an environment variable from both workflow and system environments
/// </summary>
/// <param name="variableName">The name of the environment variable</param>
/// <param name="workflowEnvironment">Optional dictionary containing workflow-level environment variables</param>
/// <returns>An EnvironmentVariableInfo object containing details about the variable from both sources</returns>
private static EnvironmentVariableInfo GetEnvironmentVariableDetails(string variableName, IDictionary<string, string> workflowEnvironment)
{
var info = new EnvironmentVariableInfo();
// Check workflow environment
bool foundInWorkflow = false;
string workflowValue = null;
if (workflowEnvironment != null && workflowEnvironment.TryGetValue(variableName, out workflowValue))
{
foundInWorkflow = true;
info.FromWorkflow = true;
info.IsTrue = StringUtil.ConvertToBoolean(workflowValue); // Workflow value takes precedence for the boolean value
}
// Also check system environment
string systemValue = Environment.GetEnvironmentVariable(variableName);
bool foundInSystem = !string.IsNullOrEmpty(systemValue);
info.FromSystem = foundInSystem;
// If not found in workflow, use system values
if (!foundInWorkflow)
{
info.IsTrue = StringUtil.ConvertToBoolean(systemValue);
}
return info;
}
}
}

View File

@@ -23,18 +23,34 @@ namespace GitHub.Runner.Listener
private RunnerSettings _settings;
private ITerminal _term;
private TimeSpan _getNextMessageRetryInterval;
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
private TaskAgentStatus _runnerStatus = TaskAgentStatus.Online;
private CancellationTokenSource _getMessagesTokenSource;
private VssCredentials _creds;
private VssCredentials _credsV2;
private TaskAgentSession _session;
private IRunnerServer _runnerServer;
private IBrokerServer _brokerServer;
private ICredentialManager _credMgr;
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
private bool _accessTokenRevoked = false;
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
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)
{
@@ -43,15 +59,29 @@ namespace GitHub.Runner.Listener
_term = HostContext.GetService<ITerminal>();
_runnerServer = HostContext.GetService<IRunnerServer>();
_brokerServer = HostContext.GetService<IBrokerServer>();
_credMgr = HostContext.GetService<ICredentialManager>();
}
public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
{
Trace.Entering();
// Settings
var configManager = HostContext.GetService<IConfigurationManager>();
_settings = configManager.LoadSettings();
// Load settings if not provided through constructor
if (_settings == null)
{
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 serverUrl = _settings.ServerUrl;
Trace.Info(_settings);
@@ -63,8 +93,7 @@ namespace GitHub.Runner.Listener
// Create connection.
Trace.Info("Loading Credentials");
var credMgr = HostContext.GetService<ICredentialManager>();
_creds = credMgr.LoadCredentials();
_creds = _credMgr.LoadCredentials(allowAuthUrlV2: false);
var agent = new TaskAgentReference
{
@@ -87,7 +116,8 @@ namespace GitHub.Runner.Listener
try
{
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");
if (!string.IsNullOrEmpty(serverUrl) &&
@@ -112,6 +142,13 @@ namespace GitHub.Runner.Listener
encounteringError = false;
}
if (!_handlerInitialized)
{
// Register event handler for auth migration state change
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
_handlerInitialized = true;
}
return CreateSessionResult.Success;
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
@@ -130,7 +167,22 @@ namespace GitHub.Runner.Listener
Trace.Error("Catch exception during create session.");
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.
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}");
if (ex is TaskAgentSessionConflictException)
@@ -161,6 +214,12 @@ namespace GitHub.Runner.Listener
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
{
_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 (_handlerInitialized)
{
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
}
if (!_accessTokenRevoked)
{
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
@@ -194,7 +258,7 @@ namespace GitHub.Runner.Listener
public void OnJobStatus(object sender, JobStatusEventArgs e)
{
Trace.Info("Received job status event. JobState: {0}", e.Status);
runnerStatus = e.Status;
_runnerStatus = e.Status;
try
{
_getMessagesTokenSource?.Cancel();
@@ -219,8 +283,15 @@ namespace GitHub.Runner.Listener
_getMessagesTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
try
{
if (_needRefreshCredsV2)
{
Trace.Info("Refreshing broker connection.");
await RefreshBrokerConnectionAsync();
_needRefreshCredsV2 = false;
}
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
runnerStatus,
_runnerStatus,
BuildConstants.RunnerPackage.Version,
VarUtil.OS,
VarUtil.OSArchitecture,
@@ -254,11 +325,11 @@ namespace GitHub.Runner.Listener
Trace.Info("Hosted runner has been deprovisioned.");
throw;
}
catch (AccessDeniedException e) when (e.ErrorCode == 1)
catch (AccessDeniedException e) when (e.ErrorCode == 1 && !HostContext.AllowAuthMigration)
{
throw;
}
catch (RunnerNotFoundException)
catch (RunnerNotFoundException) when (!HostContext.AllowAuthMigration)
{
throw;
}
@@ -267,7 +338,8 @@ namespace GitHub.Runner.Listener
Trace.Error("Catch exception during get next message.");
Trace.Error(ex);
if (!IsGetNextMessageExceptionRetriable(ex))
if (!HostContext.AllowAuthMigration &&
!IsGetNextMessageExceptionRetriable(ex))
{
throw new NonRetryableException("Get next message failed with non-retryable error.", ex);
}
@@ -298,6 +370,12 @@ namespace GitHub.Runner.Listener
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
await RefreshBrokerConnectionAsync();
@@ -329,7 +407,7 @@ namespace GitHub.Runner.Listener
}
}
public async Task RefreshListenerTokenAsync(CancellationToken cancellationToken)
public async Task RefreshListenerTokenAsync()
{
await RefreshBrokerConnectionAsync();
}
@@ -339,6 +417,21 @@ namespace GitHub.Runner.Listener
await Task.CompletedTask;
}
public async Task AcknowledgeMessageAsync(string runnerRequestId, CancellationToken cancellationToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Short timeout
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
Trace.Info($"Acknowledging runner request '{runnerRequestId}'.");
await _brokerServer.AcknowledgeRunnerRequestAsync(
runnerRequestId,
_session.SessionId,
_runnerStatus,
BuildConstants.RunnerPackage.Version,
VarUtil.OS,
VarUtil.OSArchitecture,
linkedCts.Token);
}
private bool IsGetNextMessageExceptionRetriable(Exception ex)
{
if (ex is TaskAgentNotFoundException ||
@@ -432,17 +525,16 @@ namespace GitHub.Runner.Listener
private async Task RefreshBrokerConnectionAsync()
{
var configManager = HostContext.GetService<IConfigurationManager>();
_settings = configManager.LoadSettings();
Trace.Info("Reload credentials.");
_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))
{
throw new InvalidOperationException("ServerUrlV2 is not set");
}
var credMgr = HostContext.GetService<ICredentialManager>();
VssCredentials creds = credMgr.LoadCredentials();
await _brokerServer.ConnectAsync(new Uri(_settings.ServerUrlV2), creds);
private void HandleAuthMigrationChanged(object sender, EventArgs e)
{
Trace.Info($"Auth migration changed. Current allow auth migration state: {HostContext.AllowAuthMigration}");
_needRefreshCredsV2 = true;
}
}
}

View File

@@ -25,6 +25,7 @@ namespace GitHub.Runner.Listener.Configuration
Task UnconfigureAsync(CommandSettings command);
void DeleteLocalRunnerConfig();
RunnerSettings LoadSettings();
RunnerSettings LoadMigratedSettings();
}
public sealed class ConfigurationManager : RunnerService, IConfigurationManager
@@ -66,6 +67,22 @@ namespace GitHub.Runner.Listener.Configuration
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)
{
_term.WriteLine();
@@ -127,7 +144,7 @@ namespace GitHub.Runner.Listener.Configuration
runnerSettings.ServerUrl = inputUrl;
// Get the credentials
credProvider = GetCredentialProvider(command, runnerSettings.ServerUrl);
creds = credProvider.GetVssCredentials(HostContext);
creds = credProvider.GetVssCredentials(HostContext, allowAuthUrlV2: false);
Trace.Info("legacy vss cred retrieved");
}
else
@@ -135,10 +152,9 @@ namespace GitHub.Runner.Listener.Configuration
runnerSettings.GitHubUrl = inputUrl;
registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration");
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register);
var hasLegacyUrl = !string.IsNullOrEmpty(authResult.LegacyUrl);
runnerSettings.ServerUrl = hasLegacyUrl ? authResult.LegacyUrl : authResult.TenantUrl;
runnerSettings.UseV2Flow = authResult.UseV2Flow;
Trace.Info($"Using V2 flow: {runnerSettings.UseV2Flow}");
runnerSettings.ServerUrl = authResult.TenantUrl;
runnerSettings.UseRunnerAdminFlow = authResult.UseRunnerAdminFlow;
Trace.Info($"Using runner-admin flow: {runnerSettings.UseRunnerAdminFlow}");
creds = authResult.ToVssCredentials();
Trace.Info("cred retrieved via GitHub auth");
}
@@ -195,7 +211,7 @@ namespace GitHub.Runner.Listener.Configuration
string poolName = null;
TaskAgentPool agentPool = null;
List<TaskAgentPool> agentPools;
if (runnerSettings.UseV2Flow)
if (runnerSettings.UseRunnerAdminFlow)
{
agentPools = await _dotcomServer.GetRunnerGroupsAsync(runnerSettings.GitHubUrl, registerToken);
}
@@ -243,7 +259,7 @@ namespace GitHub.Runner.Listener.Configuration
var userLabels = command.GetLabels();
_term.WriteLine();
List<TaskAgent> agents;
if (runnerSettings.UseV2Flow)
if (runnerSettings.UseRunnerAdminFlow)
{
agents = await _dotcomServer.GetRunnerByNameAsync(runnerSettings.GitHubUrl, registerToken, runnerSettings.AgentName);
}
@@ -264,7 +280,7 @@ namespace GitHub.Runner.Listener.Configuration
try
{
if (runnerSettings.UseV2Flow)
if (runnerSettings.UseRunnerAdminFlow)
{
var runner = await _dotcomServer.ReplaceRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
@@ -314,10 +330,11 @@ namespace GitHub.Runner.Listener.Configuration
try
{
if (runnerSettings.UseV2Flow)
if (runnerSettings.UseRunnerAdminFlow)
{
var runner = await _dotcomServer.AddRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
runnerSettings.UseV2Flow = true; // if we are using runner admin, we also need to hit broker
agent.Id = runner.Id;
agent.Authorization = new TaskAgentAuthorization()
@@ -367,25 +384,46 @@ namespace GitHub.Runner.Listener.Configuration
{
{ "clientId", agent.Authorization.ClientId.ToString("D") },
{ "authorizationUrl", agent.Authorization.AuthorizationUrl.AbsoluteUri },
{ "requireFipsCryptography", agent.Properties.GetValue("RequireFipsCryptography", false).ToString() }
{ "requireFipsCryptography", agent.Properties.GetValue("RequireFipsCryptography", true).ToString() }
},
};
if (agent.Properties.GetValue("EnableAuthMigrationByDefault", false) &&
agent.Properties.TryGetValue<string>("AuthorizationUrlV2", out var authUrlV2) &&
!string.IsNullOrEmpty(authUrlV2))
{
credentialData.Data["enableAuthMigrationByDefault"] = "true";
credentialData.Data["authorizationUrlV2"] = authUrlV2;
}
// Save the negotiated OAuth credential data
_store.SaveCredential(credentialData);
}
else
{
throw new NotSupportedException("Message queue listen OAuth token.");
}
// allow the server to override the serverUrlV2 and useV2Flow
if (agent.Properties.TryGetValue("ServerUrlV2", out string serverUrlV2) &&
!string.IsNullOrEmpty(serverUrlV2))
{
Trace.Info($"Service enforced serverUrlV2: {serverUrlV2}");
runnerSettings.ServerUrlV2 = serverUrlV2;
}
if (agent.Properties.TryGetValue("UseV2Flow", out bool useV2Flow) && useV2Flow)
{
Trace.Info($"Service enforced useV2Flow: {useV2Flow}");
runnerSettings.UseV2Flow = useV2Flow;
}
// Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired.
if (!runnerSettings.UseV2Flow)
if (!runnerSettings.UseV2Flow && !runnerSettings.UseRunnerAdminFlow)
{
var credMgr = HostContext.GetService<ICredentialManager>();
VssCredentials credential = credMgr.LoadCredentials();
VssCredentials credential = credMgr.LoadCredentials(allowAuthUrlV2: false);
try
{
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
@@ -405,20 +443,6 @@ namespace GitHub.Runner.Listener.Configuration
}
}
// allow the server to override the serverUrlV2 and useV2Flow
if (agent.Properties.TryGetValue("ServerUrlV2", out string serverUrlV2) &&
!string.IsNullOrEmpty(serverUrlV2))
{
Trace.Info($"Service enforced serverUrlV2: {serverUrlV2}");
runnerSettings.ServerUrlV2 = serverUrlV2;
}
if (agent.Properties.TryGetValue("UseV2Flow", out bool useV2Flow) && useV2Flow)
{
Trace.Info($"Service enforced useV2Flow: {useV2Flow}");
runnerSettings.UseV2Flow = useV2Flow;
}
_term.WriteSection("Runner settings");
// We will Combine() what's stored with root. Defaults to string a relative path
@@ -513,41 +537,50 @@ namespace GitHub.Runner.Listener.Configuration
if (isConfigured && hasCredentials)
{
RunnerSettings settings = _store.GetSettings();
var credentialManager = HostContext.GetService<ICredentialManager>();
// Get the credentials
VssCredentials creds = null;
if (string.IsNullOrEmpty(settings.GitHubUrl))
{
var credProvider = GetCredentialProvider(command, settings.ServerUrl);
creds = credProvider.GetVssCredentials(HostContext);
Trace.Info("legacy vss cred retrieved");
}
else
if (settings.UseRunnerAdminFlow)
{
var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove");
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, deletionToken, Constants.RunnerEvent.Remove);
creds = authResult.ToVssCredentials();
Trace.Info("cred retrieved via GitHub auth");
}
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
await _runnerServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
var agents = await _runnerServer.GetAgentsAsync(settings.AgentName);
Trace.Verbose("Returns {0} agents", agents.Count);
TaskAgent agent = agents.FirstOrDefault();
if (agent == null)
{
_term.WriteLine("Does not exist. Skipping " + currentAction);
await _dotcomServer.DeleteRunnerAsync(settings.GitHubUrl, deletionToken, settings.AgentId);
}
else
{
await _runnerServer.DeleteAgentAsync(settings.AgentId);
var credentialManager = HostContext.GetService<ICredentialManager>();
_term.WriteLine();
_term.WriteSuccessMessage("Runner removed successfully");
// Get the credentials
VssCredentials creds = null;
if (string.IsNullOrEmpty(settings.GitHubUrl))
{
var credProvider = GetCredentialProvider(command, settings.ServerUrl);
creds = credProvider.GetVssCredentials(HostContext, allowAuthUrlV2: false);
Trace.Info("legacy vss cred retrieved");
}
else
{
var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove");
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, deletionToken, Constants.RunnerEvent.Remove);
creds = authResult.ToVssCredentials();
Trace.Info("cred retrieved via GitHub auth");
}
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
await _runnerServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
var agents = await _runnerServer.GetAgentsAsync(settings.AgentName);
Trace.Verbose("Returns {0} agents", agents.Count);
TaskAgent agent = agents.FirstOrDefault();
if (agent == null)
{
_term.WriteLine("Does not exist. Skipping " + currentAction);
}
else
{
await _runnerServer.DeleteAgentAsync(settings.AgentId);
}
}
_term.WriteLine();
_term.WriteSuccessMessage("Runner removed successfully");
}
else
{

View File

@@ -13,7 +13,7 @@ namespace GitHub.Runner.Listener.Configuration
public interface ICredentialManager : IRunnerService
{
ICredentialProvider GetCredentialProvider(string credType);
VssCredentials LoadCredentials();
VssCredentials LoadCredentials(bool allowAuthUrlV2);
}
public class CredentialManager : RunnerService, ICredentialManager
@@ -40,7 +40,7 @@ namespace GitHub.Runner.Listener.Configuration
return creds;
}
public VssCredentials LoadCredentials()
public VssCredentials LoadCredentials(bool allowAuthUrlV2)
{
IConfigurationStore store = HostContext.GetService<IConfigurationStore>();
@@ -51,21 +51,16 @@ namespace GitHub.Runner.Listener.Configuration
CredentialData credData = store.GetCredentials();
var migratedCred = store.GetMigratedCredentials();
if (migratedCred != null)
if (migratedCred != null &&
migratedCred.Scheme == Constants.Configuration.OAuth)
{
credData = migratedCred;
// Re-write .credentials with Token URL
store.SaveCredential(credData);
// Delete .credentials_migrated
store.DeleteMigratedCredential();
}
ICredentialProvider credProv = GetCredentialProvider(credData.Scheme);
credProv.CredentialData = credData;
VssCredentials creds = credProv.GetVssCredentials(HostContext);
VssCredentials creds = credProv.GetVssCredentials(HostContext, allowAuthUrlV2);
return creds;
}
@@ -94,25 +89,16 @@ namespace GitHub.Runner.Listener.Configuration
public string Token { get; set; }
[DataMember(Name = "use_v2_flow")]
public bool UseV2Flow { get; set; }
[DataMember(Name = "legacy_url")]
public string LegacyUrl { get; set; }
[DataMember(Name = "legacy_token")]
public string LegacyToken { get; set; }
public bool UseRunnerAdminFlow { get; set; }
public VssCredentials ToVssCredentials()
{
ArgUtil.NotNullOrEmpty(TokenSchema, nameof(TokenSchema));
ArgUtil.NotNullOrEmpty(Token, nameof(Token));
var hasLegacyToken = !string.IsNullOrEmpty(LegacyToken);
var token = hasLegacyToken ? LegacyToken : Token;
if (string.Equals(TokenSchema, "OAuthAccessToken", StringComparison.OrdinalIgnoreCase))
{
return new VssCredentials(new VssOAuthAccessTokenCredential(token), CredentialPromptType.DoNotPrompt);
return new VssCredentials(new VssOAuthAccessTokenCredential(Token), CredentialPromptType.DoNotPrompt);
}
else
{

View File

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

View File

@@ -22,10 +22,18 @@ namespace GitHub.Runner.Listener.Configuration
// 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 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
var oauthEndpointUrl = this.CredentialData.Data.GetValueOrDefault("oauthEndpointUrl", authorizationUrl);

View File

@@ -110,7 +110,12 @@ namespace GitHub.Runner.Listener
{
var jwt = JsonWebToken.Create(accessToken);
var claims = jwt.ExtractClaims();
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orch_id", StringComparison.OrdinalIgnoreCase))?.Value;
if (string.IsNullOrEmpty(orchestrationId))
{
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
}
if (!string.IsNullOrEmpty(orchestrationId))
{
Trace.Info($"Pull OrchestrationId {orchestrationId} from JWT claims");

View File

@@ -32,8 +32,9 @@ namespace GitHub.Runner.Listener
Task DeleteSessionAsync();
Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token);
Task DeleteMessageAsync(TaskAgentMessage message);
Task AcknowledgeMessageAsync(string runnerRequestId, CancellationToken cancellationToken);
Task RefreshListenerTokenAsync(CancellationToken token);
Task RefreshListenerTokenAsync();
void OnJobStatus(object sender, JobStatusEventArgs e);
}
@@ -44,6 +45,7 @@ namespace GitHub.Runner.Listener
private ITerminal _term;
private IRunnerServer _runnerServer;
private IBrokerServer _brokerServer;
private ICredentialManager _credMgr;
private TaskAgentSession _session;
private TimeSpan _getNextMessageRetryInterval;
private bool _accessTokenRevoked = false;
@@ -51,11 +53,12 @@ namespace GitHub.Runner.Listener
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
private TaskAgentStatus _runnerStatus = TaskAgentStatus.Online;
private CancellationTokenSource _getMessagesTokenSource;
private VssCredentials _creds;
private bool _isBrokerSession = false;
private VssCredentials _credsV2;
private bool _needRefreshCredsV2 = false;
private bool _handlerInitialized = false;
public override void Initialize(IHostContext hostContext)
{
@@ -64,6 +67,7 @@ namespace GitHub.Runner.Listener
_term = HostContext.GetService<ITerminal>();
_runnerServer = HostContext.GetService<IRunnerServer>();
_brokerServer = hostContext.GetService<IBrokerServer>();
_credMgr = hostContext.GetService<ICredentialManager>();
}
public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
@@ -78,8 +82,7 @@ namespace GitHub.Runner.Listener
// Create connection.
Trace.Info("Loading Credentials");
var credMgr = HostContext.GetService<ICredentialManager>();
_creds = credMgr.LoadCredentials();
_creds = _credMgr.LoadCredentials(allowAuthUrlV2: false);
var agent = new TaskAgentReference
{
@@ -113,16 +116,6 @@ namespace GitHub.Runner.Listener
_settings.PoolId,
taskAgentSession,
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.");
if (encounteringError)
{
@@ -131,6 +124,13 @@ namespace GitHub.Runner.Listener
encounteringError = false;
}
if (!_handlerInitialized)
{
Trace.Info("Registering AuthMigrationChanged event handler.");
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
_handlerInitialized = true;
}
return CreateSessionResult.Success;
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
@@ -196,16 +196,16 @@ namespace GitHub.Runner.Listener
{
if (_session != null && _session.SessionId != Guid.Empty)
{
if (_handlerInitialized)
{
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
}
if (!_accessTokenRevoked)
{
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{
await _runnerServer.DeleteAgentSessionAsync(_settings.PoolId, _session.SessionId, ts.Token);
if (_isBrokerSession)
{
await _brokerServer.DeleteSessionAsync(ts.Token);
}
}
}
else
@@ -218,7 +218,7 @@ namespace GitHub.Runner.Listener
public void OnJobStatus(object sender, JobStatusEventArgs e)
{
Trace.Info("Received job status event. JobState: {0}", e.Status);
runnerStatus = e.Status;
_runnerStatus = e.Status;
try
{
_getMessagesTokenSource?.Cancel();
@@ -251,7 +251,7 @@ namespace GitHub.Runner.Listener
message = await _runnerServer.GetAgentMessageAsync(_settings.PoolId,
_session.SessionId,
_lastMessageId,
runnerStatus,
_runnerStatus,
BuildConstants.RunnerPackage.Version,
VarUtil.OS,
VarUtil.OSArchitecture,
@@ -261,14 +261,21 @@ namespace GitHub.Runner.Listener
// Decrypt the message body if the session is using encryption
message = DecryptMessage(message);
if (message != null && message.MessageType == BrokerMigrationMessage.MessageType)
{
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,
runnerStatus,
_runnerStatus,
BuildConstants.RunnerPackage.Version,
VarUtil.OS,
VarUtil.OSArchitecture,
@@ -309,11 +316,11 @@ namespace GitHub.Runner.Listener
Trace.Info("Hosted runner has been deprovisioned.");
throw;
}
catch (AccessDeniedException e) when (e.ErrorCode == 1)
catch (AccessDeniedException e) when (e.ErrorCode == 1 && !HostContext.AllowAuthMigration)
{
throw;
}
catch (RunnerNotFoundException)
catch (RunnerNotFoundException) when (!HostContext.AllowAuthMigration)
{
throw;
}
@@ -322,12 +329,19 @@ namespace GitHub.Runner.Listener
Trace.Error("Catch exception during get next message.");
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.
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.");
}
else if (!IsGetNextMessageExceptionRetriable(ex))
else if (!HostContext.AllowAuthMigration &&
!IsGetNextMessageExceptionRetriable(ex))
{
throw;
}
@@ -354,6 +368,12 @@ namespace GitHub.Runner.Listener
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
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
@@ -411,10 +431,26 @@ namespace GitHub.Runner.Listener
}
}
public async Task RefreshListenerTokenAsync(CancellationToken cancellationToken)
public async Task RefreshListenerTokenAsync()
{
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
await _brokerServer.ForceRefreshConnection(_creds);
_credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
await _brokerServer.ForceRefreshConnection(_credsV2);
}
public async Task AcknowledgeMessageAsync(string runnerRequestId, CancellationToken cancellationToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Short timeout
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
Trace.Info($"Acknowledging runner request '{runnerRequestId}'.");
await _brokerServer.AcknowledgeRunnerRequestAsync(
runnerRequestId,
_session.SessionId,
_runnerStatus,
BuildConstants.RunnerPackage.Version,
VarUtil.OS,
VarUtil.OSArchitecture,
linkedCts.Token);
}
private TaskAgentMessage DecryptMessage(TaskAgentMessage message)
@@ -545,5 +581,11 @@ namespace GitHub.Runner.Listener
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="System.IO.FileSystem.AccessControl" Version="5.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>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@@ -1,10 +1,12 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -14,7 +16,9 @@ using GitHub.Runner.Common.Util;
using GitHub.Runner.Listener.Check;
using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Sdk;
using GitHub.Services.OAuth;
using GitHub.Services.WebApi;
using GitHub.Services.WebApi.Jwt;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Listener
@@ -31,6 +35,14 @@ namespace GitHub.Runner.Listener
private ITerminal _term;
private bool _inConfigStage;
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>
// 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);
_term = HostContext.GetService<ITerminal>();
_acquireJobThrottler = HostContext.CreateService<IErrorThrottler>();
_runnerServer = HostContext.GetService<IRunnerServer>();
}
public async Task<int> ExecuteCommand(CommandSettings command)
@@ -66,6 +79,8 @@ namespace GitHub.Runner.Listener
//register a SIGTERM handler
HostContext.Unloading += Runner_Unloading;
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
// TODO Unit test to cover this logic
Trace.Info(nameof(ExecuteCommand));
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);
}
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
return await RunAsync(settings, command.RunOnce || settings.Ephemeral);
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral);
}
else
{
@@ -311,6 +335,9 @@ namespace GitHub.Runner.Listener
}
finally
{
_authMigrationClaimsCheckTokenSource?.Cancel();
_authMigrationTelemetryTokenSource?.Cancel();
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
_term.CancelKeyPress -= CtrlCHandler;
HostContext.Unloading -= Runner_Unloading;
_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)
{
Trace.Info($"Using BrokerMessageListener");
var brokerListener = new BrokerMessageListener();
var brokerListener = new BrokerMessageListener(settings, isMigratedSettings);
brokerListener.Initialize(HostContext);
return brokerListener;
}
@@ -379,15 +406,65 @@ namespace GitHub.Runner.Listener
try
{
Trace.Info(nameof(RunAsync));
_listener = GetMessageListener(settings);
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
if (createSessionResult == CreateSessionResult.SessionConflict)
// First try using migrated settings if available
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");
@@ -401,6 +478,8 @@ namespace GitHub.Runner.Listener
// Should we try to cleanup ephemeral runners
bool runOnceJobCompleted = false;
bool skipSessionDeletion = false;
bool restartSession = false; // Flag to indicate session restart
bool restartSessionPending = false;
try
{
var notification = HostContext.GetService<IJobNotification>();
@@ -416,6 +495,15 @@ namespace GitHub.Runner.Listener
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;
bool skipMessageDeletion = false;
try
@@ -566,22 +654,42 @@ namespace GitHub.Runner.Listener
else
{
var messageRef = StringUtil.ConvertFromJson<RunnerJobRequestRef>(message.Body);
// Acknowledge (best-effort)
if (messageRef.ShouldAcknowledge) // Temporary feature flag
{
try
{
await _listener.AcknowledgeMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
}
catch (Exception ex)
{
Trace.Error($"Best-effort acknowledge failed for request '{messageRef.RunnerRequestId}'");
Trace.Error(ex);
}
}
Pipelines.AgentJobRequestMessage jobRequestMessage = null;
// Create connection
var credMgr = HostContext.GetService<ICredentialManager>();
var creds = credMgr.LoadCredentials();
if (string.IsNullOrEmpty(messageRef.RunServiceUrl))
{
// Connect
var credMgr = HostContext.GetService<ICredentialManager>();
var creds = credMgr.LoadCredentials(allowAuthUrlV2: false);
var actionsRunServer = HostContext.CreateService<IActionsRunServer>();
await actionsRunServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
// Get job message
jobRequestMessage = await actionsRunServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
}
else
{
// Connect
var credMgr = HostContext.GetService<ICredentialManager>();
var credsV2 = credMgr.LoadCredentials(allowAuthUrlV2: true);
var runServer = HostContext.CreateService<IRunServer>();
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), creds);
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), credsV2);
// Get job message
try
{
jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageRef.BillingOwnerId, messageQueueLoopTokenSource.Token);
@@ -599,11 +707,21 @@ namespace GitHub.Runner.Listener
catch (Exception 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;
}
}
// Dispatch
jobDispatcher.Run(jobRequestMessage, runOnce);
// Run once?
if (runOnce)
{
Trace.Info("One time used runner received job message.");
@@ -633,7 +751,7 @@ namespace GitHub.Runner.Listener
else if (string.Equals(message.MessageType, TaskAgentMessageTypes.ForceTokenRefresh))
{
Trace.Info("Received ForceTokenRefreshMessage");
await _listener.RefreshListenerTokenAsync(messageQueueLoopTokenSource.Token);
await _listener.RefreshListenerTokenAsync();
}
else if (string.Equals(message.MessageType, RunnerRefreshConfigMessage.MessageType))
{
@@ -645,6 +763,17 @@ namespace GitHub.Runner.Listener
configType: runnerRefreshConfigMessage.ConfigType,
serviceType: runnerRefreshConfigMessage.ServiceType,
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
{
@@ -699,10 +828,16 @@ namespace GitHub.Runner.Listener
if (settings.Ephemeral && runOnceJobCompleted)
{
var configManager = HostContext.GetService<IConfigurationManager>();
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)
{
@@ -716,6 +851,220 @@ namespace GitHub.Runner.Listener
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)
{
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}'.");
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
_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)

View File

@@ -10,6 +10,9 @@ namespace GitHub.Runner.Listener
[DataMember(Name = "runner_request_id")]
public string RunnerRequestId { get; set; }
[DataMember(Name = "should_acknowledge")]
public bool ShouldAcknowledge { get; set; }
[DataMember(Name = "run_service_url")]
public string RunServiceUrl { get; set; }

View File

@@ -38,6 +38,7 @@ namespace GitHub.Runner.Sdk
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
{
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
RawClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
var rawHeaderValues = new List<ProductInfoHeaderValue>();

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />
</startup>
</configuration>

View File

@@ -18,7 +18,7 @@
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(PackageRuntime)' != 'win-arm64' ">
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>

View File

@@ -688,7 +688,8 @@ namespace GitHub.Runner.Worker
{
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
{
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
var displayHelpfulActionsDownloadErrors = executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.DisplayHelpfulActionsDownloadErrors) ?? false;
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors);
}
else
{

View File

@@ -450,7 +450,8 @@ namespace GitHub.Runner.Worker
}
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase))
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(mainToken?.Value))
{
@@ -490,7 +491,7 @@ namespace GitHub.Runner.Worker
}
else
{
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16' or 'node20' instead.");
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
}
}
else if (pluginToken != null)
@@ -501,7 +502,7 @@ namespace GitHub.Runner.Worker
};
}
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'.");
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.");
}
private void ConvertInputs(

View File

@@ -111,19 +111,19 @@ namespace GitHub.Runner.Worker.Container
{
IList<string> dockerOptions = new List<string>();
// OPTIONS
dockerOptions.Add($"--name {container.ContainerDisplayName}");
dockerOptions.Add(DockerUtil.CreateEscapedOption("--name", container.ContainerDisplayName));
dockerOptions.Add($"--label {DockerInstanceLabel}");
if (!string.IsNullOrEmpty(container.ContainerWorkDirectory))
{
dockerOptions.Add($"--workdir {container.ContainerWorkDirectory}");
dockerOptions.Add(DockerUtil.CreateEscapedOption("--workdir", container.ContainerWorkDirectory));
}
if (!string.IsNullOrEmpty(container.ContainerNetwork))
{
dockerOptions.Add($"--network {container.ContainerNetwork}");
dockerOptions.Add(DockerUtil.CreateEscapedOption("--network", container.ContainerNetwork));
}
if (!string.IsNullOrEmpty(container.ContainerNetworkAlias))
{
dockerOptions.Add($"--network-alias {container.ContainerNetworkAlias}");
dockerOptions.Add(DockerUtil.CreateEscapedOption("--network-alias", container.ContainerNetworkAlias));
}
foreach (var port in container.UserPortMappings)
{
@@ -195,10 +195,10 @@ namespace GitHub.Runner.Worker.Container
{
IList<string> dockerOptions = new List<string>();
// OPTIONS
dockerOptions.Add($"--name {container.ContainerDisplayName}");
dockerOptions.Add(DockerUtil.CreateEscapedOption("--name", container.ContainerDisplayName));
dockerOptions.Add($"--label {DockerInstanceLabel}");
dockerOptions.Add($"--workdir {container.ContainerWorkDirectory}");
dockerOptions.Add(DockerUtil.CreateEscapedOption("--workdir", container.ContainerWorkDirectory));
dockerOptions.Add($"--rm");
foreach (var env in container.ContainerEnvironmentVariables)

View File

@@ -862,7 +862,21 @@ namespace GitHub.Runner.Worker
ExpressionValues["secrets"] = Global.Variables.ToSecretsContext();
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");
var githubAccessToken = new StringContextData(Global.Variables.Get("system.github.token"));

View File

@@ -11,5 +11,10 @@ namespace GitHub.Runner.Worker
var isContainerHooksPathSet = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Constants.Hooks.ContainerHooksPath));
return isContainerHookFeatureFlagSet && isContainerHooksPathSet;
}
public static bool IsContainerActionRunnerTempEnabled(Variables variables)
{
return variables?.GetBoolean(Constants.Runner.Features.ContainerActionRunnerTemp) ?? false;
}
}
}

View File

@@ -191,11 +191,19 @@ namespace GitHub.Runner.Worker.Handlers
ArgUtil.Directory(tempWorkflowDirectory, nameof(tempWorkflowDirectory));
container.MountVolumes.Add(new MountVolume("/var/run/docker.sock", "/var/run/docker.sock"));
if (FeatureManager.IsContainerActionRunnerTempEnabled(ExecutionContext.Global.Variables))
{
container.MountVolumes.Add(new MountVolume(tempDirectory, "/github/runner_temp"));
}
container.MountVolumes.Add(new MountVolume(tempHomeDirectory, "/github/home"));
container.MountVolumes.Add(new MountVolume(tempWorkflowDirectory, "/github/workflow"));
container.MountVolumes.Add(new MountVolume(tempFileCommandDirectory, "/github/file_commands"));
container.MountVolumes.Add(new MountVolume(defaultWorkingDirectory, "/github/workspace"));
if (FeatureManager.IsContainerActionRunnerTempEnabled(ExecutionContext.Global.Variables))
{
container.AddPathTranslateMapping(tempDirectory, "/github/runner_temp");
}
container.AddPathTranslateMapping(tempHomeDirectory, "/github/home");
container.AddPathTranslateMapping(tempWorkflowDirectory, "/github/workflow");
container.AddPathTranslateMapping(tempFileCommandDirectory, "/github/file_commands");

View File

@@ -58,10 +58,41 @@ namespace GitHub.Runner.Worker.Handlers
var nodeData = data as NodeJSActionExecutionData;
// With node12 EoL in 04/2022 and node16 EoL in 09/23, we want to execute all JS actions using node20
// With node20 EoL approaching, we're preparing to migrate to node24
if (string.Equals(nodeData.NodeVersion, "node12", StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(nodeData.NodeVersion, "node16", StringComparison.InvariantCultureIgnoreCase))
{
nodeData.NodeVersion = "node20";
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20;
}
// Check if node20 was explicitly specified in the action
// We don't modify if node24 was explicitly specified
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
{
bool useNode24ByDefault = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.UseNode24ByDefaultFlag) ?? false;
bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false;
var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24);
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion);
nodeData.NodeVersion = finalNodeVersion;
if (!string.IsNullOrEmpty(configWarningMessage))
{
executionContext.Warning(configWarningMessage);
}
if (!string.IsNullOrEmpty(platformWarningMessage))
{
executionContext.Warning(platformWarningMessage);
}
// Show information about Node 24 migration in Phase 2
if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
{
string infoMessage = "Node 20 is being deprecated. This workflow is running with Node 24 by default. " +
"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable.";
executionContext.Output(infoMessage);
}
}
(handler as INodeScriptActionHandler).Data = nodeData;

View File

@@ -249,7 +249,7 @@ namespace GitHub.Runner.Worker.Handlers
{
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
resolvedScriptPath = StepHost.ResolvePathForStepHost(ExecutionContext, scriptFilePath).Replace("\"", "\\\"");
resolvedScriptPath = $"\"{StepHost.ResolvePathForStepHost(ExecutionContext, scriptFilePath).Replace("\"", "\\\"")}\"";
}
else
{
@@ -260,7 +260,7 @@ namespace GitHub.Runner.Worker.Handlers
}
scriptFilePath = Inputs["path"];
ArgUtil.NotNullOrEmpty(scriptFilePath, "path");
resolvedScriptPath = Inputs["path"].Replace("\"", "\\\"");
resolvedScriptPath = $"\"{Inputs["path"].Replace("\"", "\\\"")}\"";
}
// Format arg string with script path

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using GitHub.Runner.Sdk;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
@@ -63,10 +64,47 @@ namespace GitHub.Runner.Worker.Handlers
var append = @"if ((Test-Path -LiteralPath variable:\LASTEXITCODE)) { exit $LASTEXITCODE }";
contents = $"{prepend}{Environment.NewLine}{contents}{Environment.NewLine}{append}";
break;
case "bash":
case "sh":
contents = FixBashEnvironmentVariables(contents);
break;
}
return contents;
}
/// <summary>
/// Fixes unquoted environment variables in bash/sh scripts to prevent issues with paths containing spaces.
/// This method quotes environment variables used in shell redirects and command substitutions.
/// </summary>
/// <param name="contents">The shell script content to fix</param>
/// <returns>Fixed shell script content with properly quoted environment variables</returns>
private static string FixBashEnvironmentVariables(string contents)
{
if (string.IsNullOrEmpty(contents))
{
return contents;
}
// Pattern to match environment variables in shell redirects that aren't already quoted
// This targets patterns like: >> $GITHUB_STEP_SUMMARY, > $GITHUB_OUTPUT, etc.
// but avoids already quoted ones like: >> "$GITHUB_STEP_SUMMARY" or >> '$GITHUB_OUTPUT'
var redirectPattern = new Regex(
@"(\s+(?:>>|>|<|2>>|2>)\s+)(\$[A-Za-z_][A-Za-z0-9_]*)\b(?!\s*['""])",
RegexOptions.Compiled | RegexOptions.Multiline
);
// Replace unquoted environment variables in redirects with quoted versions
contents = redirectPattern.Replace(contents, match =>
{
var redirectOperator = match.Groups[1].Value; // e.g., " >> "
var envVar = match.Groups[2].Value; // e.g., "$GITHUB_STEP_SUMMARY"
return $"{redirectOperator}\"{envVar}\"";
});
return contents;
}
internal static (string shellCommand, string shellArgs) ParseShellOptionString(string shellOption)
{
var shellStringParts = shellOption.Split(" ", 2);

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using GitHub.DistributedTask.Pipelines.ContextData;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -9,7 +8,6 @@ using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using System.Linq;
using GitHub.Runner.Worker.Container.ContainerHooks;
using System.IO;
using System.Threading.Channels;
namespace GitHub.Runner.Worker.Handlers
@@ -60,7 +58,14 @@ namespace GitHub.Runner.Worker.Handlers
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
{
return Task.FromResult<string>(preferredVersion);
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
}
return Task.FromResult(nodeVersion);
}
public async Task<int> ExecuteAsync(IExecutionContext context,
@@ -137,8 +142,12 @@ namespace GitHub.Runner.Worker.Handlers
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
{
// Optimistically use the default
string nodeExternal = preferredVersion;
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
}
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
{
@@ -211,7 +220,7 @@ namespace GitHub.Runner.Worker.Handlers
// [OPTIONS]
dockerCommandArgs.Add($"-i");
dockerCommandArgs.Add($"--workdir {workingDirectory}");
dockerCommandArgs.Add(DockerUtil.CreateEscapedOption("--workdir", workingDirectory));
foreach (var env in environment)
{
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
@@ -264,7 +273,14 @@ namespace GitHub.Runner.Worker.Handlers
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
{
string nodeExternal = preferredVersion;
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
}
// Check for Alpine container compatibility
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
{
var os = Constants.Runner.Platform.ToString();

View File

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

@@ -50,8 +50,11 @@ namespace GitHub.Runner.Worker
if (message.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out VariableValue orchestrationId) &&
!string.IsNullOrEmpty(orchestrationId.Value))
{
// make the orchestration id the first item in the user-agent header to avoid get truncated in server log.
HostContext.UserAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", orchestrationId.Value));
if (!HostContext.UserAgents.Any(x => string.Equals(x.Product?.Name, "OrchestrationId", StringComparison.OrdinalIgnoreCase)))
{
// make the orchestration id the first item in the user-agent header to avoid get truncated in server log.
HostContext.UserAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", orchestrationId.Value));
}
// make sure orchestration id is in the user-agent header.
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
@@ -318,24 +321,17 @@ namespace GitHub.Runner.Worker
{
try
{
if (jobContext.Global.Variables.GetBoolean(Constants.Runner.Features.SkipRetryCompleteJobUponKnownErrors) ?? false)
{
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);
}
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;
}
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(ex);
exceptions.Add(ex);
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(ex);

View File

@@ -12,6 +12,12 @@
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Test</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sdk\Sdk.csproj" />
<ProjectReference Include="..\Runner.Common\Runner.Common.csproj" />
@@ -20,7 +26,7 @@
<ItemGroup>
<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="YamlDotNet.Signed" Version="5.3.0" />
</ItemGroup>

View File

@@ -106,6 +106,18 @@ namespace GitHub.Services.Common
{
VssTraceActivity traceActivity = VssTraceActivity.Current;
if (!m_appliedServerCertificateValidationCallbackToTransportHandler &&
request.RequestUri.Scheme == "https")
{
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
if (httpClientHandler != null &&
this.Settings.ServerCertificateValidationCallback != null)
{
httpClientHandler.ServerCertificateCustomValidationCallback = this.Settings.ServerCertificateValidationCallback;
}
m_appliedServerCertificateValidationCallbackToTransportHandler = true;
}
lock (m_thisLock)
{
// Ensure that we attempt to use the most appropriate authentication mechanism by default.
@@ -291,6 +303,7 @@ namespace GitHub.Services.Common
}
}
private bool m_appliedServerCertificateValidationCallbackToTransportHandler;
private readonly HttpMessageHandler m_transportHandler;
private HttpMessageInvoker m_messageInvoker;
private CredentialWrapper m_credentialWrapper;

View File

@@ -253,11 +253,12 @@ namespace GitHub.Actions.RunService.WebApi
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;

View File

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

View File

@@ -79,6 +79,7 @@ namespace GitHub.Actions.RunService.WebApi
{
queryParams.Add("status", status.Value.ToString());
}
if (runnerVersion != null)
{
queryParams.Add("runnerVersion", runnerVersion);
@@ -142,7 +143,6 @@ namespace GitHub.Actions.RunService.WebApi
}
public async Task<TaskAgentSession> CreateSessionAsync(
TaskAgentSession session,
CancellationToken cancellationToken = default)
{
@@ -191,6 +191,76 @@ namespace GitHub.Actions.RunService.WebApi
throw new Exception($"Failed to delete broker session: {result.Error}");
}
public async Task AcknowledgeRunnerRequestAsync(
string runnerRequestId,
Guid? sessionId,
string runnerVersion,
TaskAgentStatus? status,
string os = null,
string architecture = null,
CancellationToken cancellationToken = default)
{
// URL
var requestUri = new Uri(Client.BaseAddress, "acknowledge");
// Query parameters
List<KeyValuePair<string, string>> queryParams = new List<KeyValuePair<string, string>>();
if (sessionId != null)
{
queryParams.Add("sessionId", sessionId.Value.ToString());
}
if (status != null)
{
queryParams.Add("status", status.Value.ToString());
}
if (runnerVersion != null)
{
queryParams.Add("runnerVersion", runnerVersion);
}
if (os != null)
{
queryParams.Add("os", os);
}
if (architecture != null)
{
queryParams.Add("architecture", architecture);
}
// Body
var payload = new Dictionary<string, string>
{
["runnerRequestId"] = runnerRequestId,
};
var requestContent = new ObjectContent<Dictionary<string, string>>(payload, new VssJsonMediaTypeFormatter(true));
// POST
var result = await SendAsync<object>(
new HttpMethod("POST"),
requestUri: requestUri,
queryParameters: queryParams,
content: requestContent,
readErrorBody: true,
cancellationToken: cancellationToken);
if (result.IsSuccess)
{
return;
}
if (TryParseErrorBody(result.ErrorBody, out BrokerError brokerError))
{
switch (brokerError.ErrorKind)
{
case BrokerErrorKind.RunnerNotFound:
throw new RunnerNotFoundException(brokerError.Message);
default:
break;
}
}
throw new Exception($"Failed to acknowledge runner request. Request to {requestUri} failed with status: {result.StatusCode}. Error message {result.Error}");
}
private static bool TryParseErrorBody(string errorBody, out BrokerError error)
{
if (!string.IsNullOrEmpty(errorBody))

View File

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

View File

@@ -29,7 +29,7 @@ namespace GitHub.Services.Launch.Contracts
{
[DataMember(EmitDefaultValue = false, Name = "authentication")]
public ActionDownloadAuthenticationResponse Authentication { get; set; }
[DataMember(EmitDefaultValue = false, Name = "package_details")]
public ActionDownloadPackageDetailsResponse PackageDetails { get; set; }
@@ -64,7 +64,7 @@ namespace GitHub.Services.Launch.Contracts
[DataContract]
public class ActionDownloadPackageDetailsResponse
public class ActionDownloadPackageDetailsResponse
{
[DataMember(EmitDefaultValue = false, Name = "version")]
public string Version { get; set; }
@@ -81,4 +81,25 @@ namespace GitHub.Services.Launch.Contracts
[DataMember(EmitDefaultValue = false, Name = "actions")]
public IDictionary<string, ActionDownloadInfoResponse> Actions { get; set; }
}
[DataContract]
public class ActionDownloadResolutionError
{
/// <summary>
/// The error message associated with the action download error.
/// </summary>
[DataMember(EmitDefaultValue = false, Name = "message")]
public string Message { get; set; }
}
[DataContract]
public class ActionDownloadResolutionErrorCollection
{
/// <summary>
/// A mapping of action specifications to their download errors.
/// <remarks>The key is the full name of the action plus version, e.g. "actions/checkout@v2".</remarks>
/// </summary>
[DataMember(EmitDefaultValue = false, Name = "errors")]
public IDictionary<string, ActionDownloadResolutionError> Errors { get; set; }
}
}

View File

@@ -2,6 +2,7 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
@@ -32,11 +33,52 @@ namespace GitHub.Services.Launch.Client
public async Task<ActionDownloadInfoCollection> GetResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken)
{
var GetResolveActionsDownloadInfoURLEndpoint = new Uri(m_launchServiceUrl, $"/actions/build/{planId.ToString()}/jobs/{jobId.ToString()}/runnerresolve/actions");
return ToServerData(await GetLaunchSignedURLResponse<ActionReferenceRequestList, ActionDownloadInfoResponseCollection>(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken));
var response = await GetLaunchSignedURLResponse<ActionReferenceRequestList>(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken);
return ToServerData(await ReadJsonContentAsync<ActionDownloadInfoResponseCollection>(response, cancellationToken));
}
// Resolve Actions
private async Task<T> GetLaunchSignedURLResponse<R, T>(Uri uri, R request, CancellationToken cancellationToken)
public async Task<ActionDownloadInfoCollection> GetResolveActionsDownloadInfoAsyncV2(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken)
{
var GetResolveActionsDownloadInfoURLEndpoint = new Uri(m_launchServiceUrl, $"/actions/build/{planId.ToString()}/jobs/{jobId.ToString()}/runnerresolve/actions");
var response = await GetLaunchSignedURLResponse<ActionReferenceRequestList>(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken);
if (response.IsSuccessStatusCode)
{
// Success response - deserialize the action download info
return ToServerData(await ReadJsonContentAsync<ActionDownloadInfoResponseCollection>(response, cancellationToken));
}
var responseError = response.ReasonPhrase ?? "";
if (response.StatusCode == HttpStatusCode.UnprocessableEntity)
{
// 422 response - unresolvable actions, error details are in the body
var errors = await ReadJsonContentAsync<ActionDownloadResolutionErrorCollection>(response, cancellationToken);
string combinedErrorMessage;
if (errors?.Errors != null && errors.Errors.Any())
{
combinedErrorMessage = String.Join(". ", errors.Errors.Select(kvp => kvp.Value.Message));
}
else
{
combinedErrorMessage = responseError;
}
throw new UnresolvableActionDownloadInfoException(combinedErrorMessage);
}
else if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
// Here we want to add a message so customers don't think it's a rate limit scoped to them
// Ideally this would be 500 but the runner retries 500s, which we don't want to do when we're being rate limited
// See: https://github.com/github/ecosystem-api/issues/4084
throw new NonRetryableActionDownloadInfoException(responseError + " (GitHub has reached an internal rate limit, please try again later)");
}
else
{
throw new Exception(responseError);
}
}
private async Task<HttpResponseMessage> GetLaunchSignedURLResponse<R>(Uri uri, R request, CancellationToken cancellationToken)
{
using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri))
{
@@ -46,10 +88,7 @@ namespace GitHub.Services.Launch.Client
using (HttpContent content = new ObjectContent<R>(request, m_formatter))
{
requestMessage.Content = content;
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
{
return await ReadJsonContentAsync<T>(response, cancellationToken);
}
return await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken);
}
}
}

View File

@@ -520,8 +520,8 @@ namespace GitHub.Services.Results.Client
Number = r.Order.GetValueOrDefault(),
Name = r.Name,
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat),
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat),
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture),
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture),
Conclusion = ConvertResultToConclusion(r.Result)
};
}

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)
{
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.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
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 = "")
{
_tokenSource = new CancellationTokenSource();

View File

@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -18,8 +19,6 @@ namespace GitHub.Runner.Common.Tests.Listener
private readonly Mock<IBrokerServer> _brokerServer;
private readonly Mock<IRunnerServer> _runnerServer;
private readonly Mock<ICredentialManager> _credMgr;
private Mock<IConfigurationStore> _store;
public BrokerMessageListenerL0()
{
@@ -27,7 +26,6 @@ namespace GitHub.Runner.Common.Tests.Listener
_config = new Mock<IConfigurationManager>();
_config.Setup(x => x.LoadSettings()).Returns(_settings);
_credMgr = new Mock<ICredentialManager>();
_store = new Mock<IConfigurationStore>();
_brokerServer = new Mock<IBrokerServer>();
_runnerServer = new Mock<IRunnerServer>();
}
@@ -35,7 +33,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void CreatesSession()
public async Task CreatesSession()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
@@ -50,9 +48,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
// Act.
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 = "")
{
TestHostContext tc = new(this, testName);
tc.SetSingleton<IConfigurationManager>(_config.Object);
tc.SetSingleton<ICredentialManager>(_credMgr.Object);
tc.SetSingleton<IConfigurationStore>(_store.Object);
tc.SetSingleton<IBrokerServer>(_brokerServer.Object);
tc.SetSingleton<IRunnerServer>(_runnerServer.Object);
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.Services.Common;
using GitHub.Services.OAuth;
using Moq;
using Xunit;
namespace GitHub.Runner.Common.Tests.Listener.Configuration
{
public class TestRunnerCredential : CredentialProvider
{
public TestRunnerCredential() : base("TEST") { }
public override VssCredentials GetVssCredentials(IHostContext context)
public override VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2)
{
Tracing trace = context.GetTrace("OuthAccessToken");
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]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void CreatesSession()
public async Task CreatesSession()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
@@ -67,7 +67,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token))
.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.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -95,69 +95,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void CreatesSessionWithBrokerMigration()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
var expectedSession = new TaskAgentSession()
{
OwnerName = "legacy",
BrokerMigrationMessage = new BrokerMigrationMessage(new Uri("https://broker.actions.github.com"))
};
var expectedBrokerSession = new TaskAgentSession()
{
OwnerName = "broker"
};
_runnerServer
.Setup(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedBrokerSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
// Act.
MessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result);
// Assert.
Assert.Equal(CreateSessionResult.Success, result);
_runnerServer
.Verify(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
_brokerServer
.Verify(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void DeleteSession()
public async Task DeleteSession()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
@@ -177,7 +115,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token))
.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.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -204,84 +142,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void DeleteSessionWithBrokerMigration()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
{
Tracing trace = tc.GetTrace();
// Arrange.
var expectedSession = new TaskAgentSession()
{
OwnerName = "legacy",
BrokerMigrationMessage = new BrokerMigrationMessage(new Uri("https://broker.actions.github.com"))
};
var expectedBrokerSession = new TaskAgentSession()
{
SessionId = Guid.NewGuid(),
OwnerName = "broker"
};
_runnerServer
.Setup(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedSession));
_brokerServer
.Setup(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token))
.Returns(Task.FromResult(expectedBrokerSession));
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
// Act.
MessageListener listener = new();
listener.Initialize(tc);
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
trace.Info("result: {0}", result);
Assert.Equal(CreateSessionResult.Success, result);
_runnerServer
.Verify(x => x.CreateAgentSessionAsync(
_settings.PoolId,
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
_brokerServer
.Verify(x => x.CreateSessionAsync(
It.Is<TaskAgentSession>(y => y != null),
tokenSource.Token), Times.Once());
_brokerServer
.Setup(x => x.DeleteSessionAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
// Act.
await listener.DeleteSessionAsync();
//Assert
_runnerServer
.Verify(x => x.DeleteAgentSessionAsync(
_settings.PoolId, expectedBrokerSession.SessionId, It.IsAny<CancellationToken>()), Times.Once());
_brokerServer
.Verify(x => x.DeleteSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void GetNextMessage()
public async Task GetNextMessage()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
@@ -301,7 +162,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token))
.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.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -362,7 +223,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void GetNextMessageWithBrokerMigration()
public async Task GetNextMessageWithBrokerMigration()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
@@ -382,7 +243,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token))
.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.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -462,13 +323,22 @@ namespace GitHub.Runner.Common.Tests.Listener
_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.Never);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void CreateSessionWithOriginalCredential()
public async Task CreateSessionWithOriginalCredential()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
@@ -484,7 +354,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token))
.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 };
originalCred.Data["authorizationUrl"] = "https://s.server";
@@ -513,7 +383,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void SkipDeleteSession_WhenGetNextMessageGetTaskAgentAccessTokenExpiredException()
public async Task SkipDeleteSession_WhenGetNextMessageGetTaskAgentAccessTokenExpiredException()
{
using (TestHostContext tc = CreateTestContext())
using (var tokenSource = new CancellationTokenSource())
@@ -533,7 +403,7 @@ namespace GitHub.Runner.Common.Tests.Listener
tokenSource.Token))
.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.GetMigratedCredentials()).Returns(default(CredentialData));
@@ -571,5 +441,301 @@ namespace GitHub.Runner.Common.Tests.Listener
_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.Text;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Listener;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Listener;
using GitHub.Runner.Sdk;
using Moq;
using Xunit;
using System.Threading;
using GitHub.Runner.Common.Tests;
using System.Text;
namespace GitHub.Runner.Tests.Listener
{
@@ -210,9 +210,9 @@ namespace GitHub.Runner.Tests.Listener
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);
hc.EnableAuthMigration("L0Test");
var validRunnerQualifiedId = "valid/runner/qualifiedid/1";
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.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.False(hc.AllowAuthMigration);
}
}
@@ -306,7 +307,7 @@ namespace GitHub.Runner.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async Task UpdateRunnerConfigAsync_RefreshRunnerCredetialsFailure_ShouldReportTelemetry()
public async Task UpdateRunnerConfigAsync_RefreshRunnerCredentialsFailure_ShouldReportTelemetry()
{
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]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
@@ -575,5 +626,53 @@ namespace GitHub.Runner.Tests.Listener
_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 GitHub.Runner.Listener;
using GitHub.Runner.Listener.Configuration;
using Moq;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
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 Moq;
using Xunit;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Common.Tests.Listener
@@ -24,6 +26,9 @@ namespace GitHub.Runner.Common.Tests.Listener
private Mock<IConfigurationStore> _configStore;
private Mock<ISelfUpdater> _updater;
private Mock<IErrorThrottler> _acquireJobThrottler;
private Mock<ICredentialManager> _credentialManager;
private Mock<IActionsRunServer> _actionsRunServer;
private Mock<IRunServer> _runServer;
public RunnerL0()
{
@@ -37,6 +42,9 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore = new Mock<IConfigurationStore>();
_updater = new Mock<ISelfUpdater>();
_acquireJobThrottler = new Mock<IErrorThrottler>();
_credentialManager = new Mock<ICredentialManager>();
_actionsRunServer = new Mock<IActionsRunServer>();
_runServer = new Mock<IRunServer>();
}
private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName)
@@ -57,7 +65,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
//process 2 new job messages, and one cancel message
public async void TestRunAsync()
public async Task TestRunAsync()
{
using (var hc = new TestHostContext(this))
{
@@ -169,7 +177,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[MemberData(nameof(RunAsServiceTestData))]
[Trait("Level", "L0")]
[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))
{
@@ -177,6 +185,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
var command = new CommandSettings(hc, args);
@@ -201,7 +210,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void TestMachineProvisionerCLI()
public async Task TestMachineProvisionerCLI()
{
using (var hc = new TestHostContext(this))
{
@@ -209,6 +218,7 @@ namespace GitHub.Runner.Common.Tests.Listener
hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IMessageListener>(_messageListener.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
var command = new CommandSettings(hc, new[] { "run" });
@@ -235,7 +245,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void TestRunOnce()
public async Task TestRunOnce()
{
using (var hc = new TestHostContext(this))
{
@@ -332,7 +342,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void TestRunOnceOnlyTakeOneJobMessage()
public async Task TestRunOnceOnlyTakeOneJobMessage()
{
using (var hc = new TestHostContext(this))
{
@@ -433,7 +443,7 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void TestRunOnceHandleUpdateMessage()
public async Task TestRunOnceHandleUpdateMessage()
{
using (var hc = new TestHostContext(this))
{
@@ -523,13 +533,14 @@ namespace GitHub.Runner.Common.Tests.Listener
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
public async void TestRemoveLocalRunnerConfig()
public async Task TestRemoveLocalRunnerConfig()
{
using (var hc = new TestHostContext(this))
{
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
hc.SetSingleton<IPromptManager>(_promptManager.Object);
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
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());
}
}
[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.AtLeast(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,126 @@
using GitHub.Actions.RunService.WebApi;
using GitHub.DistributedTask.WebApi;
using GitHub.Services.Launch.Client;
using GitHub.Services.Launch.Contracts;
using Moq;
using Moq.Protected;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace GitHub.Actions.RunService.WebApi.Tests
{
public sealed class LaunchHttpClientL0
{
[Fact]
public async Task GetResolveActionsDownloadInfoAsync_SuccessResponse()
{
var baseUrl = new Uri("https://api.github.com/");
var planId = Guid.NewGuid();
var jobId = Guid.NewGuid();
var token = "fake-token";
var actionReferenceList = new ActionReferenceList
{
Actions = new List<ActionReference>
{
new ActionReference
{
NameWithOwner = "owner1/action1",
Ref = "0123456789"
}
}
};
var responseContent = @"{
""actions"": {
""owner1/action1@0123456789"": {
""name"": ""owner1/action1"",
""resolved_name"": ""owner1/action1"",
""resolved_sha"": ""0123456789"",
""version"": ""0123456789"",
""zip_url"": ""https://github.com/owner1/action1/zip"",
""tar_url"": ""https://github.com/owner1/action1/tar""
}
}
}";
var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(responseContent, Encoding.UTF8, "application/json"),
RequestMessage = new HttpRequestMessage()
{
RequestUri = new Uri($"{baseUrl}actions/build/{planId}/jobs/{jobId}/runnerresolve/actions")
}
};
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(httpResponse);
var client = new LaunchHttpClient(baseUrl, mockHandler.Object, token, false);
var result = await client.GetResolveActionsDownloadInfoAsyncV2(planId, jobId, actionReferenceList, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result.Actions);
Assert.Equal(actionReferenceList.Actions.Count, result.Actions.Count);
Assert.True(result.Actions.ContainsKey("owner1/action1@0123456789"));
}
[Fact]
public async Task GetResolveActionsDownloadInfoAsync_UnprocessableEntityResponse()
{
var baseUrl = new Uri("https://api.github.com/");
var planId = Guid.NewGuid();
var jobId = Guid.NewGuid();
var token = "fake-token";
var actionReferenceList = new ActionReferenceList
{
Actions = new List<ActionReference>
{
new ActionReference
{
NameWithOwner = "owner1/action1",
Ref = "0123456789"
}
}
};
var responseContent = @"{
""errors"": {
""owner1/invalid-action@0123456789"": {
""message"": ""Unable to resolve action 'owner1/invalid-action@0123456789', repository not found""
}
}
}";
var httpResponse = new HttpResponseMessage(HttpStatusCode.UnprocessableEntity)
{
Content = new StringContent(responseContent, Encoding.UTF8, "application/json"),
RequestMessage = new HttpRequestMessage()
{
RequestUri = new Uri($"{baseUrl}actions/build/{planId}/jobs/{jobId}/runnerresolve/actions")
}
};
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(httpResponse);
var client = new LaunchHttpClient(baseUrl, mockHandler.Object, token, false);
var exception = await Assert.ThrowsAsync<UnresolvableActionDownloadInfoException>(
() => client.GetResolveActionsDownloadInfoAsyncV2(planId, jobId, actionReferenceList, CancellationToken.None));
Assert.Contains("repository not found", exception.Message);
}
}
}

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

View File

@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using Xunit;
namespace GitHub.Runner.Common.Tests.Util
{
public class NodeUtilL0
{
// We're testing the logic with feature flags
[Theory]
[InlineData(false, false, false, false, "node20", false)] // Phase 1: No env vars
[InlineData(false, false, false, true, "node20", false)] // Phase 1: Allow unsecure (redundant)
[InlineData(false, false, true, false, "node24", false)] // Phase 1: Force node24
[InlineData(false, false, true, true, "node20", true)] // Phase 1: Both flags (use phase default + warning)
[InlineData(false, true, false, false, "node24", false)] // Phase 2: No env vars
[InlineData(false, true, false, true, "node20", false)] // Phase 2: Allow unsecure
[InlineData(false, true, true, false, "node24", false)] // Phase 2: Force node24 (redundant)
[InlineData(false, true, true, true, "node24", true)] // Phase 2: Both flags (use phase default + warning)
[InlineData(true, false, false, false, "node24", false)] // Phase 3: Always Node 24 regardless of env vars
[InlineData(true, false, false, true, "node24", false)] // Phase 3: Always Node 24 regardless of env vars
[InlineData(true, false, true, false, "node24", false)] // Phase 3: Always Node 24 regardless of env vars
[InlineData(true, false, true, true, "node24", false)] // Phase 3: Always Node 24 regardless of env vars, no warnings in Phase 3
public void TestNodeVersionLogic(bool requireNode24, bool useNode24ByDefault, bool forceNode24, bool allowUnsecureNode, string expectedVersion, bool expectWarning)
{
try
{
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.ForceNode24Variable, forceNode24 ? "true" : null);
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, allowUnsecureNode ? "true" : null);
// Call the actual method
var (actualVersion, warningMessage) = NodeUtil.DetermineActionsNodeVersion(null, useNode24ByDefault, requireNode24);
// Assert
Assert.Equal(expectedVersion, actualVersion);
if (expectWarning)
{
Assert.NotNull(warningMessage);
Assert.Contains("Both", warningMessage);
Assert.Contains("are set to true", warningMessage);
}
else
{
Assert.Null(warningMessage);
}
}
finally
{
// Cleanup
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.ForceNode24Variable, null);
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, null);
}
}
[Theory]
[InlineData(false, false, false, false, false, true, "node20", false)] // Phase 1: System env: none, Workflow env: allow=true
[InlineData(false, false, true, false, false, false, "node24", false)] // Phase 1: System env: force node24, Workflow env: none
[InlineData(false, true, false, false, true, false, "node24", false)] // Phase 1: System env: none, Workflow env: force node24
[InlineData(false, false, false, true, false, true, "node20", false)] // Phase 1: System env: allow=true, Workflow env: allow=true (workflow takes precedence)
[InlineData(false, false, true, true, false, false, "node20", true)] // Phase 1: System env: both true, Workflow env: none (use phase default + warning)
[InlineData(false, false, false, false, true, true, "node20", true)] // Phase 1: System env: none, Workflow env: both (use phase default + warning)
[InlineData(true, false, false, false, false, false, "node24", false)] // Phase 2: System env: none, Workflow env: none
[InlineData(true, false, false, true, false, false, "node20", false)] // Phase 2: System env: allow=true, Workflow env: none
[InlineData(true, false, false, false, false, true, "node20", false)] // Phase 2: System env: none, Workflow env: allow unsecure
[InlineData(true, false, true, false, false, true, "node20", false)] // Phase 2: System env: force node24, Workflow env: allow unsecure
[InlineData(true, false, true, true, false, false, "node24", true)] // Phase 2: System env: both true, Workflow env: none (use phase default + warning)
[InlineData(true, false, false, false, true, true, "node24", true)] // Phase 2: System env: none, Workflow env: both (phase default + warning)
[InlineData(false, true, false, false, false, true, "node24", false)] // Phase 3: System env: none, Workflow env: allow=true (always Node 24 in Phase 3)
[InlineData(false, true, true, true, false, false, "node24", false)] // Phase 3: System env: both true, Workflow env: none (always Node 24 in Phase 3, no warning)
[InlineData(false, true, false, false, true, true, "node24", false)] // Phase 3: System env: none, Workflow env: both (always Node 24 in Phase 3, no warning)
public void TestNodeVersionLogicWithWorkflowEnvironment(bool useNode24ByDefault, bool requireNode24,
bool systemForceNode24, bool systemAllowUnsecure,
bool workflowForceNode24, bool workflowAllowUnsecure,
string expectedVersion, bool expectWarning)
{
try
{
// Set system environment variables
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.ForceNode24Variable, systemForceNode24 ? "true" : null);
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, systemAllowUnsecure ? "true" : null);
// Set workflow environment variables
var workflowEnv = new Dictionary<string, string>();
if (workflowForceNode24)
{
workflowEnv[Constants.Runner.NodeMigration.ForceNode24Variable] = "true";
}
if (workflowAllowUnsecure)
{
workflowEnv[Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable] = "true";
}
// Call the actual method with our test parameters
var (actualVersion, warningMessage) = NodeUtil.DetermineActionsNodeVersion(workflowEnv, useNode24ByDefault, requireNode24);
// Assert
Assert.Equal(expectedVersion, actualVersion);
if (expectWarning)
{
Assert.NotNull(warningMessage);
Assert.Contains("Both", warningMessage);
Assert.Contains("are set to true", warningMessage);
}
else
{
Assert.Null(warningMessage);
}
}
finally
{
// Cleanup
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.ForceNode24Variable, null);
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, null);
}
}
}
}

View File

@@ -1659,6 +1659,76 @@ runs:
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void LoadsNode24ActionDefinition()
{
try
{
// Arrange.
Setup();
const string Content = @"
# Container action
name: 'Hello World'
description: 'Greet the world and record the time'
author: 'GitHub'
inputs:
greeting: # id of input
description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout'
required: true
default: 'Hello'
entryPoint: # id of input
description: 'optional docker entrypoint overwrite.'
required: false
outputs:
time: # id of output
description: 'The time we did the greeting'
icon: 'hello.svg' # vector art to display in the GitHub Marketplace
color: 'green' # optional, decorates the entry in the GitHub Marketplace
runs:
using: 'node24'
main: 'task.js'
";
Pipelines.ActionStep instance;
string directory;
CreateAction(yamlContent: Content, instance: out instance, directory: out directory);
// Act.
Definition definition = _actionManager.LoadAction(_ec.Object, instance);
// Assert.
Assert.NotNull(definition);
Assert.Equal(directory, definition.Directory);
Assert.NotNull(definition.Data);
Assert.NotNull(definition.Data.Inputs); // inputs
Dictionary<string, string> inputDefaults = new(StringComparer.OrdinalIgnoreCase);
foreach (var input in definition.Data.Inputs)
{
var name = input.Key.AssertString("key").Value;
var value = input.Value.AssertScalar("value").ToString();
_hc.GetTrace().Info($"Default: {name} = {value}");
inputDefaults[name] = value;
}
Assert.Equal(2, inputDefaults.Count);
Assert.True(inputDefaults.ContainsKey("greeting"));
Assert.Equal("Hello", inputDefaults["greeting"]);
Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"]));
Assert.NotNull(definition.Data.Execution); // execution
Assert.NotNull(definition.Data.Execution as NodeJSActionExecutionData);
Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script);
Assert.Equal("node24", (definition.Data.Execution as NodeJSActionExecutionData).NodeVersion);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -2411,8 +2481,8 @@ runs:
});
_launchServer = new Mock<ILaunchServer>();
_launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>()))
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) =>
_launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors) =>
{
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
foreach (var action in actions.Actions)

View File

@@ -502,6 +502,49 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Load_Node24Action()
{
try
{
//Arrange
Setup();
var actionManifest = new ActionManifestManager();
actionManifest.Initialize(_hc);
//Act
var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "node24action.yml"));
//Assert
Assert.Equal("Hello World", result.Name);
Assert.Equal("Greet the world and record the time", result.Description);
Assert.Equal(2, result.Inputs.Count);
Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value);
Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value);
Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value);
Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value);
Assert.Equal(1, result.Deprecated.Count);
Assert.True(result.Deprecated.ContainsKey("greeting"));
result.Deprecated.TryGetValue("greeting", out string value);
Assert.Equal("This property has been deprecated", value);
Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType);
var nodeAction = result.Execution as NodeJSActionExecutionData;
Assert.Equal("main.js", nodeAction.Script);
Assert.Equal("node24", nodeAction.NodeVersion);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -758,7 +801,7 @@ namespace GitHub.Runner.Common.Tests.Worker
//Assert
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
Assert.Contains($"Failed to load {action_path}", err.Message);
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
}
finally
{

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)
{
foreach (var key in expect.Keys.ToList())

View File

@@ -33,6 +33,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[InlineData("node12", "node20")]
[InlineData("node16", "node20")]
[InlineData("node20", "node20")]
[InlineData("node24", "node24")]
public void IsNodeVersionUpgraded(string inputVersion, string expectedVersion)
{
using (TestHostContext hc = CreateTestContext())
@@ -41,7 +42,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var hf = new HandlerFactory();
hf.Initialize(hc);
// Server Feature Flag
// Setup variables
var variables = new Dictionary<string, VariableValue>();
Variables serverVariables = new(hc, variables);
@@ -72,5 +73,48 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal(expectedVersion, handler.Data.NodeVersion);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node24ExplicitlyRequested_HonoredByDefault()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
// Basic variables setup
var variables = new Dictionary<string, VariableValue>();
Variables serverVariables = new(hc, variables);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>()
});
// Act - Node 24 explicitly requested in action.yml
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node24";
var handler = hf.Create(
_ec.Object,
new ScriptReference(),
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
// Assert - should be node24 as requested
Assert.Equal("node24", handler.Data.NodeVersion);
}
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker.Handlers
{
public sealed class NodeHandlerL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void NodeJSActionExecutionDataSupportsNode24()
{
// Create NodeJSActionExecutionData with node24
var nodeJSData = new NodeJSActionExecutionData
{
NodeVersion = "node24",
Script = "test.js"
};
// Act & Assert
Assert.Equal("node24", nodeJSData.NodeVersion);
Assert.Equal(ActionExecutionType.NodeJS, nodeJSData.ExecutionType);
}
}
}

View File

@@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker.Handlers
{
public sealed class ScriptHandlerL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ScriptPath_WithSpaces_ShouldBeQuoted()
{
// Arrange - Test the path quoting logic that our fix addresses
var tempPathWithSpaces = "/path with spaces/_temp";
var scriptPathWithSpaces = Path.Combine(tempPathWithSpaces, "test-script.sh");
// Test the original (broken) behavior
var originalPath = scriptPathWithSpaces.Replace("\"", "\\\"");
// Test our fix - properly quoted path
var quotedPath = $"\"{scriptPathWithSpaces.Replace("\"", "\\\"")}\"";
// Assert
Assert.False(originalPath.StartsWith("\""), "Original path should not be quoted");
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Fixed path should be properly quoted");
Assert.Contains("path with spaces", quotedPath, StringComparison.Ordinal);
// Verify the path is properly quoted (platform-agnostic check)
Assert.True(quotedPath.StartsWith("\"/path with spaces/_temp"), "Path should start with quoted temp directory");
Assert.True(quotedPath.EndsWith("test-script.sh\""), "Path should end with quoted script name");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ScriptPath_WithQuotes_ShouldEscapeQuotes()
{
// Arrange - Test paths that contain quotes
var pathWithQuotes = "/path/\"quoted folder\"/script.sh";
// Test our fix - properly escape quotes and wrap in quotes
var quotedPath = $"\"{pathWithQuotes.Replace("\"", "\\\"")}\"";
// Assert
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Path should be wrapped in quotes");
Assert.Contains("\\\"", quotedPath, StringComparison.Ordinal);
Assert.Contains("quoted folder", quotedPath, StringComparison.Ordinal);
// Verify quotes are properly escaped
Assert.Contains("\\\"quoted folder\\\"", quotedPath, StringComparison.Ordinal);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ScriptPath_ActionsRunnerWithSpaces_ShouldBeQuoted()
{
// Arrange - Test the specific real-world scenario that was failing
var runnerPathWithSpaces = "/Users/user/Downloads/actions-runner-osx-arm64-2.328.0 2";
var tempPath = Path.Combine(runnerPathWithSpaces, "_work", "_temp");
var scriptPath = Path.Combine(tempPath, "script-guid.sh");
// Test our fix
var quotedPath = $"\"{scriptPath.Replace("\"", "\\\"")}\"";
// Assert
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Path should be wrapped in quotes");
Assert.Contains("actions-runner-osx-arm64-2.328.0 2", quotedPath, StringComparison.Ordinal);
Assert.Contains("_work", quotedPath, StringComparison.Ordinal);
Assert.Contains("_temp", quotedPath, StringComparison.Ordinal);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ScriptPath_MultipleSpaces_ShouldBeQuoted()
{
// Arrange - Test paths with multiple spaces
var pathWithMultipleSpaces = "/path/with multiple spaces/script.sh";
// Test our fix
var quotedPath = $"\"{pathWithMultipleSpaces.Replace("\"", "\\\"")}\"";
// Assert
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Path should be wrapped in quotes");
Assert.Contains("multiple spaces", quotedPath, StringComparison.Ordinal);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ScriptPath_WithoutSpaces_ShouldStillBeQuoted()
{
// Arrange - Test normal paths without spaces (regression test)
var normalPath = "/home/user/runner/_work/_temp/script.sh";
// Test our fix
var quotedPath = $"\"{normalPath.Replace("\"", "\\\"")}\"";
// Assert
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Path should be wrapped in quotes");
Assert.Equal($"\"{normalPath}\"", quotedPath);
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("/path with spaces/script.sh")]
[InlineData("/Users/user/Downloads/actions-runner-osx-arm64-2.328.0 2/_work/_temp/guid.sh")]
[InlineData("C:\\Program Files\\GitHub Runner\\script.cmd")]
[InlineData("/path/\"with quotes\"/script.sh")]
[InlineData("/path/with'single'quotes/script.sh")]
public void ScriptPath_VariousScenarios_ShouldBeProperlyQuoted(string inputPath)
{
// Arrange & Act
var quotedPath = $"\"{inputPath.Replace("\"", "\\\"")}\"";
// Assert
Assert.True(quotedPath.StartsWith("\""), "Path should start with quote");
Assert.True(quotedPath.EndsWith("\""), "Path should end with quote");
// Ensure the original path content is preserved
var unquotedContent = quotedPath.Substring(1, quotedPath.Length - 2);
if (inputPath.Contains("\""))
{
// If original had quotes, they should be escaped in the result
Assert.Contains("\\\"", unquotedContent);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void FixUpScriptContents_BashEnvironmentVariables_ShouldQuoteRedirects()
{
// Arrange
var scriptContent = @"echo ""## Dependency Status Report"" >> $GITHUB_STEP_SUMMARY
echo ""Generated on: $(date)"" >> $GITHUB_STEP_SUMMARY
echo ""| Component | Status |"" > $GITHUB_OUTPUT
echo ""npm-status=ok"" >> $GITHUB_OUTPUT";
// Act
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("bash", scriptContent);
// Assert
Assert.Contains(">> \"$GITHUB_STEP_SUMMARY\"", fixedContent);
Assert.Contains("> \"$GITHUB_OUTPUT\"", fixedContent);
Assert.DoesNotContain(">> $GITHUB_STEP_SUMMARY", fixedContent);
Assert.DoesNotContain("> $GITHUB_OUTPUT", fixedContent);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void FixUpScriptContents_AlreadyQuotedVariables_ShouldNotDoubleQuote()
{
// Arrange
var scriptContent = @"echo ""test"" >> ""$GITHUB_STEP_SUMMARY""
echo ""test"" > '$GITHUB_OUTPUT'
echo ""test"" 2>> ""$GITHUB_ENV""";
// Act
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("bash", scriptContent);
// Assert - Should remain unchanged
Assert.Equal(scriptContent, fixedContent);
Assert.Contains(">> \"$GITHUB_STEP_SUMMARY\"", fixedContent);
Assert.Contains("> '$GITHUB_OUTPUT'", fixedContent);
Assert.Contains("2>> \"$GITHUB_ENV\"", fixedContent);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void FixUpScriptContents_ShellRedirectOperators_ShouldHandleAllTypes()
{
// Arrange
var scriptContent = @"echo ""test"" >> $VAR1
echo ""test"" > $VAR2
cat < $VAR3
echo ""test"" 2>> $VAR4
echo ""test"" 2> $VAR5";
// Act
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("sh", scriptContent);
// Assert
Assert.Contains(">> \"$VAR1\"", fixedContent);
Assert.Contains("> \"$VAR2\"", fixedContent);
Assert.Contains("< \"$VAR3\"", fixedContent);
Assert.Contains("2>> \"$VAR4\"", fixedContent);
Assert.Contains("2> \"$VAR5\"", fixedContent);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void FixUpScriptContents_NonShellTypes_ShouldNotModifyEnvironmentVariables()
{
// Arrange
var scriptContent = @"echo ""test"" >> $GITHUB_STEP_SUMMARY";
// Act
var powershellFixed = ScriptHandlerHelpers.FixUpScriptContents("powershell", scriptContent);
var cmdFixed = ScriptHandlerHelpers.FixUpScriptContents("cmd", scriptContent);
var pythonFixed = ScriptHandlerHelpers.FixUpScriptContents("python", scriptContent);
// Assert - Should not modify environment variables for non-shell types
Assert.Contains(">> $GITHUB_STEP_SUMMARY", powershellFixed);
Assert.Contains(">> $GITHUB_STEP_SUMMARY", cmdFixed);
Assert.Contains(">> $GITHUB_STEP_SUMMARY", pythonFixed);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void FixUpScriptContents_ComplexScript_ShouldQuoteOnlyUnquotedRedirects()
{
// Arrange
var scriptContent = @"#!/bin/bash
# This is a test script
echo ""Starting workflow"" >> $GITHUB_STEP_SUMMARY
echo ""Already quoted"" >> ""$GITHUB_OUTPUT""
export MY_VAR=""$HOME/path with spaces""
curl -s https://api.github.com/rate_limit > $TEMP_FILE
echo ""Final status"" 2>> $ERROR_LOG
if [ -f ""$GITHUB_ENV"" ]; then
echo ""MY_VAR=test"" >> $GITHUB_ENV
fi";
// Act
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("bash", scriptContent);
// Assert
Assert.Contains(">> \"$GITHUB_STEP_SUMMARY\"", fixedContent);
Assert.Contains(">> \"$GITHUB_OUTPUT\"", fixedContent); // Should remain quoted
Assert.Contains("> \"$TEMP_FILE\"", fixedContent);
Assert.Contains("2>> \"$ERROR_LOG\"", fixedContent);
Assert.Contains(">> \"$GITHUB_ENV\"", fixedContent);
// Other parts should remain unchanged
Assert.Contains("#!/bin/bash", fixedContent);
Assert.Contains("# This is a test script", fixedContent);
Assert.Contains("export MY_VAR=\"$HOME/path with spaces\"", fixedContent);
Assert.Contains("if [ -f \"$GITHUB_ENV\" ]; then", fixedContent);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void FixUpScriptContents_EnvironmentVariablesInCommands_ShouldNotQuote()
{
// Arrange - Environment variables not in redirects should not be touched
var scriptContent = @"echo $GITHUB_STEP_SUMMARY
cd $HOME
ls -la $TEMP_DIR
if [ ""$MY_VAR"" == ""test"" ]; then
echo ""match""
fi";
// Act
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("bash", scriptContent);
// Assert - Should remain unchanged as these are not redirects
Assert.Equal(scriptContent, fixedContent);
}
}
}

View File

@@ -896,5 +896,173 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("not-working", match.Message);
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]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]

View File

@@ -162,6 +162,60 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("node20", nodeVersion);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DetermineNode24RuntimeVersionInAlpineContainerAsync()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var sh = new ContainerStepHost();
sh.Initialize(hc);
sh.Container = new ContainerInfo() { ContainerId = "1234abcd" };
_dc.Setup(d => d.DockerExec(_ec.Object, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<List<string>>()))
.Callback((IExecutionContext ec, string id, string options, string command, List<string> output) =>
{
output.Add("alpine");
})
.ReturnsAsync(0);
// Act.
var nodeVersion = await sh.DetermineNodeRuntimeVersion(_ec.Object, "node24");
// Assert.
Assert.Equal("node24_alpine", nodeVersion);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DetermineNode24RuntimeVersionInUnknownContainerAsync()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var sh = new ContainerStepHost();
sh.Initialize(hc);
sh.Container = new ContainerInfo() { ContainerId = "1234abcd" };
_dc.Setup(d => d.DockerExec(_ec.Object, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<List<string>>()))
.Callback((IExecutionContext ec, string id, string options, string command, List<string> output) =>
{
output.Add("github");
})
.ReturnsAsync(0);
// Act.
var nodeVersion = await sh.DetermineNodeRuntimeVersion(_ec.Object, "node24");
// Assert.
Assert.Equal("node24", nodeVersion);
}
}
#endif
}
}

View File

@@ -0,0 +1,63 @@
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Handlers;
using Moq;
using System;
using System.Runtime.InteropServices;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class StepHostNodeVersionL0
{
private Mock<IExecutionContext> _ec;
private DefaultStepHost _defaultStepHost;
public StepHostNodeVersionL0()
{
_ec = new Mock<IExecutionContext>();
_defaultStepHost = new DefaultStepHost();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_Node24OnArm32Linux()
{
// Test via NodeUtil directly
string preferredVersion = "node24";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
// On ARM32 Linux, we should fall back to node20
bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true;
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isArm32 && isLinux)
{
// Should downgrade to node20 on ARM32 Linux
Assert.Equal("node20", nodeVersion);
Assert.NotNull(warningMessage);
Assert.Contains("Node 24 is not supported on Linux ARM32 platforms", warningMessage);
}
else
{
// On non-ARM32 platforms, should pass through the version unmodified
Assert.Equal("node24", nodeVersion);
Assert.Null(warningMessage);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_PassThroughNonNode24Versions()
{
string preferredVersion = "node20";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
// Should never modify the version for non-node24 inputs
Assert.Equal("node20", nodeVersion);
Assert.Null(warningMessage);
}
}
}

View File

@@ -15,9 +15,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.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.Threading.ThreadPool" Version="4.3.0" />
<PackageReference Include="Moq" Version="4.20.72" />

View File

@@ -0,0 +1,20 @@
name: 'Hello World'
description: 'Greet the world and record the time'
author: 'Test Corporation'
inputs:
greeting: # id of input
description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout'
required: true
default: 'Hello'
deprecationMessage: 'This property has been deprecated'
entryPoint: # id of input
description: 'optional docker entrypoint overwrite.'
required: false
outputs:
time: # id of output
description: 'The time we did the greeting'
icon: 'hello.svg' # vector art to display in the GitHub Marketplace
color: 'green' # optional, decorates the entry in the GitHub Marketplace
runs:
using: 'node24'
main: 'main.js'

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