mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f9a9110ad | ||
|
|
0ebdf9e83d | ||
|
|
6543bf206b | ||
|
|
a942627965 | ||
|
|
83539166c9 | ||
|
|
1c1e8bfd18 | ||
|
|
59177fa379 | ||
|
|
2d7635a7f0 | ||
|
|
0203cf24d3 | ||
|
|
5e74a4d8e4 | ||
|
|
6ca97eeb88 | ||
|
|
8a9b96806d | ||
|
|
dc9cf684c9 | ||
|
|
c765c990b9 | ||
|
|
ed48ddd08c | ||
|
|
a1e6ad8d2e | ||
|
|
14856e63bc | ||
|
|
0d24afa114 | ||
|
|
20912234a5 | ||
|
|
5969cbe208 | ||
|
|
9f57d37642 | ||
|
|
60563d82d1 | ||
|
|
097ada9374 | ||
|
|
9b457781d6 | ||
|
|
9709b69571 | ||
|
|
acf3f2ba12 | ||
|
|
f03fcc8a01 | ||
|
|
e4e103c5ed | ||
|
|
a906ec302b | ||
|
|
d9e714496d | ||
|
|
df189ba6e3 | ||
|
|
4c1de69e1c | ||
|
|
26185d43d0 | ||
|
|
e911d2908d | ||
|
|
ce4b7f4dd6 | ||
|
|
505fa60905 | ||
|
|
57459ad274 | ||
|
|
890e43f6c5 | ||
|
|
3a27ca292a | ||
|
|
282f7cd2b2 | ||
|
|
f060fe5c85 | ||
|
|
1a092a24a3 | ||
|
|
26eff8e55a | ||
|
|
d7cfd2e341 | ||
|
|
a3a7b6a77e | ||
|
|
db6005b0a7 | ||
|
|
9155c42c09 | ||
|
|
1c319b4d42 | ||
|
|
fe10d4ae82 | ||
|
|
27d9c886ab | ||
|
|
5106d6578e | ||
|
|
d5ccbd10d1 | ||
|
|
f1b5b5bd5c | ||
|
|
aaf1b92847 | ||
|
|
c1095ae2d1 | ||
|
|
a0a0a76378 | ||
|
|
d47013928b | ||
|
|
cdeec012aa | ||
|
|
2cb1f9431a | ||
|
|
e86c9487ab | ||
|
|
dc9695f123 |
@@ -4,7 +4,7 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
|
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
|
||||||
"ghcr.io/devcontainers/features/dotnet": {
|
"ghcr.io/devcontainers/features/dotnet": {
|
||||||
"version": "8.0.407"
|
"version": "8.0.412"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20"
|
"version": "20"
|
||||||
|
|||||||
25
.github/copilot-instructions.md
vendored
Normal file
25
.github/copilot-instructions.md
vendored
Normal 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).
|
||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
devScript: ./dev.sh
|
devScript: ./dev.sh
|
||||||
|
|
||||||
- runtime: win-x64
|
- runtime: win-x64
|
||||||
os: windows-2019
|
os: windows-latest
|
||||||
devScript: ./dev
|
devScript: ./dev
|
||||||
|
|
||||||
- runtime: win-arm64
|
- runtime: win-arm64
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
|
|||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
144
.github/workflows/docker-buildx-upgrade.yml
vendored
Normal file
144
.github/workflows/docker-buildx-upgrade.yml
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
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
|
||||||
|
pr_body="Upgrades Docker version from ${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }} to ${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Docker Buildx version from ${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }} to ${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}.\n\n"
|
||||||
|
pr_body+="Release notes: https://docs.docker.com/engine/release-notes/\n\n"
|
||||||
|
pr_body+="---\n\nAutogenerated by [Docker/Buildx Version Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/docker-buildx-upgrade.yml)"
|
||||||
|
|
||||||
|
gh pr create -B main -H "$branch_name" \
|
||||||
|
--title "$pr_title" \
|
||||||
|
--body "$pr_body"
|
||||||
4
.github/workflows/dotnet-upgrade.yml
vendored
4
.github/workflows/dotnet-upgrade.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
|
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: Get current major minor version
|
- name: Get current major minor version
|
||||||
id: fetch_current_version
|
id: fetch_current_version
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
|
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
|
|||||||
42
.github/workflows/release.yml
vendored
42
.github/workflows/release.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
|
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# Make sure ./releaseVersion match ./src/runnerversion
|
# Make sure ./releaseVersion match ./src/runnerversion
|
||||||
# Query GitHub release ensure version is not used
|
# Query GitHub release ensure version is not used
|
||||||
@@ -20,7 +20,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
const core = require('@actions/core')
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
|
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
|
||||||
const releaseVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '')
|
const releaseVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '')
|
||||||
@@ -30,7 +29,7 @@ jobs:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const release = await github.repos.getReleaseByTag({
|
const release = await github.rest.repos.getReleaseByTag({
|
||||||
owner: '${{ github.event.repository.owner.name }}',
|
owner: '${{ github.event.repository.owner.name }}',
|
||||||
repo: '${{ github.event.repository.name }}',
|
repo: '${{ github.event.repository.name }}',
|
||||||
tag: 'v' + runnerVersion
|
tag: 'v' + runnerVersion
|
||||||
@@ -78,7 +77,7 @@ jobs:
|
|||||||
devScript: ./dev.sh
|
devScript: ./dev.sh
|
||||||
|
|
||||||
- runtime: win-x64
|
- runtime: win-x64
|
||||||
os: windows-2019
|
os: windows-latest
|
||||||
devScript: ./dev
|
devScript: ./dev
|
||||||
|
|
||||||
- runtime: win-arm64
|
- runtime: win-arm64
|
||||||
@@ -87,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
@@ -130,41 +129,41 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# Download runner package tar.gz/zip produced by 'build' job
|
# Download runner package tar.gz/zip produced by 'build' job
|
||||||
- name: Download Artifact (win-x64)
|
- name: Download Artifact (win-x64)
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-packages-win-x64
|
name: runner-packages-win-x64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (win-arm64)
|
- name: Download Artifact (win-arm64)
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-packages-win-arm64
|
name: runner-packages-win-arm64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (osx-x64)
|
- name: Download Artifact (osx-x64)
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-packages-osx-x64
|
name: runner-packages-osx-x64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (osx-arm64)
|
- name: Download Artifact (osx-arm64)
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-packages-osx-arm64
|
name: runner-packages-osx-arm64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (linux-x64)
|
- name: Download Artifact (linux-x64)
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-packages-linux-x64
|
name: runner-packages-linux-x64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (linux-arm)
|
- name: Download Artifact (linux-arm)
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-packages-linux-arm
|
name: runner-packages-linux-arm
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (linux-arm64)
|
- name: Download Artifact (linux-arm64)
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-packages-linux-arm64
|
name: runner-packages-linux-arm64
|
||||||
path: ./
|
path: ./
|
||||||
@@ -176,7 +175,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
const core = require('@actions/core')
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
|
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
|
||||||
var releaseNote = fs.readFileSync('${{ github.workspace }}/releaseNote.md', 'utf8').replace(/<RUNNER_VERSION>/g, runnerVersion)
|
var releaseNote = fs.readFileSync('${{ github.workspace }}/releaseNote.md', 'utf8').replace(/<RUNNER_VERSION>/g, runnerVersion)
|
||||||
@@ -216,7 +214,7 @@ jobs:
|
|||||||
|
|
||||||
# Upload release assets (full runner packages)
|
# Upload release assets (full runner packages)
|
||||||
- name: Upload Release Asset (win-x64)
|
- name: Upload Release Asset (win-x64)
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -226,7 +224,7 @@ jobs:
|
|||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
- name: Upload Release Asset (win-arm64)
|
- name: Upload Release Asset (win-arm64)
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -236,7 +234,7 @@ jobs:
|
|||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
- name: Upload Release Asset (linux-x64)
|
- name: Upload Release Asset (linux-x64)
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -246,7 +244,7 @@ jobs:
|
|||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
- name: Upload Release Asset (osx-x64)
|
- name: Upload Release Asset (osx-x64)
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -256,7 +254,7 @@ jobs:
|
|||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
- name: Upload Release Asset (osx-arm64)
|
- name: Upload Release Asset (osx-arm64)
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -266,7 +264,7 @@ jobs:
|
|||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
- name: Upload Release Asset (linux-arm)
|
- name: Upload Release Asset (linux-arm)
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -276,7 +274,7 @@ jobs:
|
|||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
|
|
||||||
- name: Upload Release Asset (linux-arm64)
|
- name: Upload Release Asset (linux-arm64)
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
uses: actions/upload-release-asset@v1.0.2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -298,7 +296,7 @@ jobs:
|
|||||||
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Compute image version
|
- name: Compute image version
|
||||||
id: image
|
id: image
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -20,6 +20,20 @@ Runner releases:
|
|||||||
|
|
||||||
 [Pre-reqs](docs/start/envlinux.md) | [Download](https://github.com/actions/runner/releases)
|
 [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 we’re working on and what stage they’re 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.
|
||||||
|
|||||||
@@ -250,6 +250,42 @@ Two problem matchers can be used:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Default from path
|
||||||
|
|
||||||
|
The problem matcher can specify a `fromPath` property at the top level, which applies when a specific pattern doesn't provide a value for `fromPath`. This is useful for tools that don't include project file information in their output.
|
||||||
|
|
||||||
|
For example, given the following compiler output that doesn't include project file information:
|
||||||
|
|
||||||
|
```
|
||||||
|
ClassLibrary.cs(16,24): warning CS0612: 'ClassLibrary.Helpers.MyHelper.Name' is obsolete
|
||||||
|
```
|
||||||
|
|
||||||
|
A problem matcher with a default from path can be used:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "csc-minimal",
|
||||||
|
"fromPath": "ClassLibrary/ClassLibrary.csproj",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+)\\((\\d+),(\\d+)\\): (error|warning) (.+): (.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"code": 5,
|
||||||
|
"message": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures that the file is rooted to the correct path when there's not enough information in the error messages to extract a `fromPath`.
|
||||||
|
|
||||||
#### Mitigate regular expression denial of service (ReDos)
|
#### Mitigate regular expression denial of service (ReDos)
|
||||||
|
|
||||||
If a matcher exceeds a 1 second timeout when processing a line, retry up to two three times total.
|
If a matcher exceeds a 1 second timeout when processing a line, retry up to two three times total.
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
Make sure the built-in node.js has access to GitHub.com or GitHub Enterprise Server.
|
Make sure the built-in node.js has access to GitHub.com or GitHub Enterprise Server.
|
||||||
|
|
||||||
The runner carries 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`
|
> Not the `node` from `$PATH`
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Supported Distributions and Versions
|
## 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
|
## Install .Net Core 3.x Linux Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## 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)
|
## [More .Net Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/macos-prerequisites?tabs=netcore30)
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## 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)
|
## [More .NET Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore30)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy AS build
|
|||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RUNNER_VERSION
|
ARG RUNNER_VERSION
|
||||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.6.1
|
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||||
ARG DOCKER_VERSION=28.0.1
|
ARG DOCKER_VERSION=28.3.2
|
||||||
ARG BUILDX_VERSION=0.21.2
|
ARG BUILDX_VERSION=0.26.1
|
||||||
|
|
||||||
RUN apt update -y && apt install curl unzip -y
|
RUN apt update -y && apt install curl unzip -y
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,20 @@
|
|||||||
## What's Changed
|
## What's Changed
|
||||||
* Bump docker/login-action from 2 to 3 by @dependabot in https://github.com/actions/runner/pull/3673
|
* Update Docker to v28.3.2 and Buildx to v0.26.1 by @github-actions[bot] in https://github.com/actions/runner/pull/3953
|
||||||
* Bump actions/stale from 8 to 9 by @dependabot in https://github.com/actions/runner/pull/3554
|
* Fix if statement structure in update script and variable reference by @salmanmkc in https://github.com/actions/runner/pull/3956
|
||||||
* Bump docker/build-push-action from 3 to 6 by @dependabot in https://github.com/actions/runner/pull/3674
|
* Add V2 flow for runner deletion by @Samirat in https://github.com/actions/runner/pull/3954
|
||||||
* update node version from 20.18.0 -> 20.18.2 by @aiqiaoy in https://github.com/actions/runner/pull/3682
|
* Node 20 -> Node 24 migration feature flagging, opt-in and opt-out environment variables by @salmanmkc in https://github.com/actions/runner/pull/3948
|
||||||
* Pass BillingOwnerId through Acquire/Complete calls by @luketomlinson in https://github.com/actions/runner/pull/3689
|
* Update Node20 and Node24 to latest by @djs-intel in https://github.com/actions/runner/pull/3972
|
||||||
* Do not retry CompleteJobAsync for known non-retryable errors by @ericsciple in https://github.com/actions/runner/pull/3696
|
* Redirect supported OS doc section to current public Docs location by @corycalahan in https://github.com/actions/runner/pull/3979
|
||||||
* Update dotnet sdk to latest version @8.0.406 by @github-actions in https://github.com/actions/runner/pull/3712
|
* Bump Microsoft.NET.Test.Sdk from 17.13.0 to 17.14.1 by @dependabot[bot] in https://github.com/actions/runner/pull/3975
|
||||||
* Update Dockerfile with new docker and buildx versions by @thboop in https://github.com/actions/runner/pull/3680
|
* Bump Azure.Storage.Blobs from 12.24.0 to 12.25.0 by @dependabot[bot] in https://github.com/actions/runner/pull/3974
|
||||||
* chore: remove redundant words by @finaltrip in https://github.com/actions/runner/pull/3705
|
* Bump actions/download-artifact from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/3973
|
||||||
* fix: actions feedback link is incorrect by @Yaminyam in https://github.com/actions/runner/pull/3165
|
* Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/3982
|
||||||
* 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
|
|
||||||
|
|
||||||
## New Contributors
|
## New Contributors
|
||||||
* @finaltrip made their first contribution in https://github.com/actions/runner/pull/3705
|
* @Samirat made their first contribution in https://github.com/actions/runner/pull/3954
|
||||||
* @Yaminyam made their first contribution in https://github.com/actions/runner/pull/3165
|
* @djs-intel made their first contribution in https://github.com/actions/runner/pull/3972
|
||||||
* @Blizter made their first contribution in https://github.com/actions/runner/pull/3750
|
|
||||||
* @GhadimiR made their first contribution in https://github.com/actions/runner/pull/3749
|
|
||||||
* @tecimovic made their first contribution in https://github.com/actions/runner/pull/3595
|
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.322.0...v2.323.0
|
**Full Changelog**: https://github.com/actions/runner/compare/v2.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.
|
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
||||||
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<Update to ./src/runnerversion when creating release>
|
2.328.0
|
||||||
|
|||||||
13
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
13
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
@@ -716,9 +716,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -4751,9 +4752,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ NODE_URL=https://nodejs.org/dist
|
|||||||
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
|
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
|
||||||
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
||||||
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
||||||
NODE20_VERSION="20.19.0"
|
NODE20_VERSION="20.19.4"
|
||||||
|
NODE24_VERSION="24.5.0"
|
||||||
|
|
||||||
get_abs_path() {
|
get_abs_path() {
|
||||||
# exploits the fact that pwd will print abs path when no args
|
# exploits the fact that pwd will print abs path when no args
|
||||||
@@ -139,6 +140,8 @@ function acquireExternalTool() {
|
|||||||
if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then
|
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.exe" node20/bin
|
||||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" 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
|
if [[ "$PRECACHE" != "" ]]; then
|
||||||
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
|
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
|
||||||
fi
|
fi
|
||||||
@@ -149,6 +152,8 @@ if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then
|
|||||||
# todo: replace these with official release when available
|
# 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.exe" node20/bin
|
||||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" 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
|
if [[ "$PRECACHE" != "" ]]; then
|
||||||
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
|
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
|
||||||
fi
|
fi
|
||||||
@@ -157,21 +162,26 @@ fi
|
|||||||
# Download the external tools only for OSX.
|
# Download the external tools only for OSX.
|
||||||
if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then
|
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${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
|
fi
|
||||||
|
|
||||||
if [[ "$PACKAGERUNTIME" == "osx-arm64" ]]; then
|
if [[ "$PACKAGERUNTIME" == "osx-arm64" ]]; then
|
||||||
# node.js v12 doesn't support macOS on arm64.
|
# 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${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
|
fi
|
||||||
|
|
||||||
# Download the external tools for Linux PACKAGERUNTIMEs.
|
# Download the external tools for Linux PACKAGERUNTIMEs.
|
||||||
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
|
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
|
||||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-x64.tar.gz" node20 fix_nested_dir
|
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_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
|
fi
|
||||||
|
|
||||||
if [[ "$PACKAGERUNTIME" == "linux-arm64" ]]; then
|
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${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
|
fi
|
||||||
|
|
||||||
if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then
|
if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then
|
||||||
|
|||||||
@@ -3299,7 +3299,7 @@ function expand(str, isTop) {
|
|||||||
var isOptions = m.body.indexOf(',') >= 0;
|
var isOptions = m.body.indexOf(',') >= 0;
|
||||||
if (!isSequence && !isOptions) {
|
if (!isSequence && !isOptions) {
|
||||||
// {a},b}
|
// {a},b}
|
||||||
if (m.post.match(/,.*\}/)) {
|
if (m.post.match(/,(?!,).*\}/)) {
|
||||||
str = m.pre + '{' + m.body + escClose + m.post;
|
str = m.pre + '{' + m.body + escClose + m.post;
|
||||||
return expand(str);
|
return expand(str);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ fi
|
|||||||
# fix upgrade issue with macOS when running as a service
|
# fix upgrade issue with macOS when running as a service
|
||||||
attemptedtargetedfix=0
|
attemptedtargetedfix=0
|
||||||
currentplatform=$(uname | awk '{print tolower($0)}')
|
currentplatform=$(uname | awk '{print tolower($0)}')
|
||||||
if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
if [[ "$currentplatform" == 'darwin' && $restartinteractiverunner -eq 0 ]]; then
|
||||||
# We needed a fix for https://github.com/actions/runner/issues/743
|
# We needed a fix for https://github.com/actions/runner/issues/743
|
||||||
# We will recreate the ./externals/nodeXY/bin/node of the past runner version that launched the runnerlistener service
|
# We will recreate the ./externals/nodeXY/bin/node of the past runner version that launched the runnerlistener service
|
||||||
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
|
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
|
||||||
@@ -135,16 +135,22 @@ if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
|||||||
then
|
then
|
||||||
# inspect the open file handles to find the node process
|
# inspect the open file handles to find the node process
|
||||||
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
|
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
|
||||||
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-)
|
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
|
then
|
||||||
nodever="node16"
|
nodever="node20"
|
||||||
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
|
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
|
then
|
||||||
nodever="node12"
|
nodever="node16"
|
||||||
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
|
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
|
||||||
fi
|
fi
|
||||||
if [[ $? -eq 0 && -n "$path" ]]
|
if [[ $? -eq 0 && -n "$path" ]]
|
||||||
|
|||||||
13
src/Runner.Common/AuthMigration.cs
Normal file
13
src/Runner.Common/AuthMigration.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public async Task ConnectAsync(Uri serverUri, VssCredentials credentials)
|
public async Task ConnectAsync(Uri serverUri, VssCredentials credentials)
|
||||||
{
|
{
|
||||||
|
Trace.Entering();
|
||||||
_brokerUri = serverUri;
|
_brokerUri = serverUri;
|
||||||
|
|
||||||
_connection = VssUtil.CreateRawConnection(serverUri, credentials);
|
_connection = VssUtil.CreateRawConnection(serverUri, credentials);
|
||||||
@@ -88,7 +89,12 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public Task ForceRefreshConnection(VssCredentials credentials)
|
public Task ForceRefreshConnection(VssCredentials credentials)
|
||||||
{
|
{
|
||||||
return ConnectAsync(_brokerUri, credentials);
|
if (!string.IsNullOrEmpty(_brokerUri?.AbsoluteUri))
|
||||||
|
{
|
||||||
|
return ConnectAsync(_brokerUri, credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldRetryException(Exception ex)
|
public bool ShouldRetryException(Exception ex)
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ namespace GitHub.Runner.Common
|
|||||||
bool IsConfigured();
|
bool IsConfigured();
|
||||||
bool IsServiceConfigured();
|
bool IsServiceConfigured();
|
||||||
bool HasCredentials();
|
bool HasCredentials();
|
||||||
|
bool IsMigratedConfigured();
|
||||||
CredentialData GetCredentials();
|
CredentialData GetCredentials();
|
||||||
CredentialData GetMigratedCredentials();
|
CredentialData GetMigratedCredentials();
|
||||||
RunnerSettings GetSettings();
|
RunnerSettings GetSettings();
|
||||||
@@ -198,6 +199,14 @@ namespace GitHub.Runner.Common
|
|||||||
return serviceConfigured;
|
return serviceConfigured;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsMigratedConfigured()
|
||||||
|
{
|
||||||
|
Trace.Info("IsMigratedConfigured()");
|
||||||
|
bool configured = new FileInfo(_migratedConfigFilePath).Exists;
|
||||||
|
Trace.Info("IsMigratedConfigured: {0}", configured);
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
public CredentialData GetCredentials()
|
public CredentialData GetCredentials()
|
||||||
{
|
{
|
||||||
if (_creds == null)
|
if (_creds == null)
|
||||||
|
|||||||
@@ -155,15 +155,36 @@ namespace GitHub.Runner.Common
|
|||||||
public const int RunnerUpdating = 3;
|
public const int RunnerUpdating = 3;
|
||||||
public const int RunOnceRunnerUpdating = 4;
|
public const int RunOnceRunnerUpdating = 4;
|
||||||
public const int SessionConflict = 5;
|
public const int SessionConflict = 5;
|
||||||
|
// Temporary error code to indicate that the runner configuration has been refreshed
|
||||||
|
// and the runner should be restarted. This is a temporary code and will be removed in the future after
|
||||||
|
// the runner is migrated to runner admin.
|
||||||
|
public const int RunnerConfigurationRefreshed = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Features
|
public static class Features
|
||||||
{
|
{
|
||||||
public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
|
public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
|
||||||
public static readonly string LogTemplateErrorsAsDebugMessages = "DistributedTask.LogTemplateErrorsAsDebugMessages";
|
public static readonly string LogTemplateErrorsAsDebugMessages = "DistributedTask.LogTemplateErrorsAsDebugMessages";
|
||||||
public static readonly string SkipRetryCompleteJobUponKnownErrors = "actions_skip_retry_complete_job_upon_known_errors";
|
|
||||||
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
|
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
|
||||||
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
|
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
|
||||||
|
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
|
||||||
|
public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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";
|
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using System.Threading.Tasks;
|
|||||||
using GitHub.DistributedTask.Logging;
|
using GitHub.DistributedTask.Logging;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Services.WebApi.Jwt;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
namespace GitHub.Runner.Common
|
||||||
{
|
{
|
||||||
@@ -37,6 +38,11 @@ namespace GitHub.Runner.Common
|
|||||||
void ShutdownRunner(ShutdownReason reason);
|
void ShutdownRunner(ShutdownReason reason);
|
||||||
void WritePerfCounter(string counter);
|
void WritePerfCounter(string counter);
|
||||||
void LoadDefaultUserAgents();
|
void LoadDefaultUserAgents();
|
||||||
|
|
||||||
|
bool AllowAuthMigration { get; }
|
||||||
|
void EnableAuthMigration(string trace);
|
||||||
|
void DeferAuthMigration(TimeSpan deferred, string trace);
|
||||||
|
event EventHandler<AuthMigrationEventArgs> AuthMigrationChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum StartupType
|
public enum StartupType
|
||||||
@@ -70,12 +76,21 @@ namespace GitHub.Runner.Common
|
|||||||
private RunnerWebProxy _webProxy = new();
|
private RunnerWebProxy _webProxy = new();
|
||||||
private string _hostType = string.Empty;
|
private string _hostType = string.Empty;
|
||||||
|
|
||||||
|
// disable auth migration by default
|
||||||
|
private readonly ManualResetEventSlim _allowAuthMigration = new ManualResetEventSlim(false);
|
||||||
|
private DateTime _deferredAuthMigrationTime = DateTime.MaxValue;
|
||||||
|
private readonly object _authMigrationLock = new object();
|
||||||
|
private CancellationTokenSource _authMigrationAutoReenableTaskCancellationTokenSource = new();
|
||||||
|
private Task _authMigrationAutoReenableTask;
|
||||||
|
|
||||||
public event EventHandler Unloading;
|
public event EventHandler Unloading;
|
||||||
|
public event EventHandler<AuthMigrationEventArgs> AuthMigrationChanged;
|
||||||
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
|
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
|
||||||
public ShutdownReason RunnerShutdownReason { get; private set; }
|
public ShutdownReason RunnerShutdownReason { get; private set; }
|
||||||
public ISecretMasker SecretMasker => _secretMasker;
|
public ISecretMasker SecretMasker => _secretMasker;
|
||||||
public List<ProductInfoHeaderValue> UserAgents => _userAgents;
|
public List<ProductInfoHeaderValue> UserAgents => _userAgents;
|
||||||
public RunnerWebProxy WebProxy => _webProxy;
|
public RunnerWebProxy WebProxy => _webProxy;
|
||||||
|
public bool AllowAuthMigration => _allowAuthMigration.IsSet;
|
||||||
public HostContext(string hostType, string logFile = null)
|
public HostContext(string hostType, string logFile = null)
|
||||||
{
|
{
|
||||||
// Validate args.
|
// Validate args.
|
||||||
@@ -207,6 +222,71 @@ namespace GitHub.Runner.Common
|
|||||||
LoadDefaultUserAgents();
|
LoadDefaultUserAgents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// marked as internal for testing
|
||||||
|
internal async Task AuthMigrationAuthReenableAsync(TimeSpan refreshInterval, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_trace.Verbose($"Auth migration defer timer is set to expire at {_deferredAuthMigrationTime.ToString("O")}. AllowAuthMigration: {_allowAuthMigration.IsSet}.");
|
||||||
|
await Task.Delay(refreshInterval, token);
|
||||||
|
if (!_allowAuthMigration.IsSet && DateTime.UtcNow > _deferredAuthMigrationTime)
|
||||||
|
{
|
||||||
|
_trace.Info($"Auth migration defer timer expired. Allowing auth migration.");
|
||||||
|
EnableAuthMigration("Auth migration defer timer expired.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Task was cancelled, exit the loop.
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_trace.Info("Error in auth migration reenable task.");
|
||||||
|
_trace.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnableAuthMigration(string trace)
|
||||||
|
{
|
||||||
|
_allowAuthMigration.Set();
|
||||||
|
|
||||||
|
lock (_authMigrationLock)
|
||||||
|
{
|
||||||
|
if (_authMigrationAutoReenableTask == null)
|
||||||
|
{
|
||||||
|
var refreshIntervalInMS = 60 * 1000;
|
||||||
|
#if DEBUG
|
||||||
|
// For L0, we will refresh faster
|
||||||
|
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL")))
|
||||||
|
{
|
||||||
|
refreshIntervalInMS = int.Parse(Environment.GetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL"));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
_authMigrationAutoReenableTask = AuthMigrationAuthReenableAsync(TimeSpan.FromMilliseconds(refreshIntervalInMS), _authMigrationAutoReenableTaskCancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_trace.Info($"Enable auth migration at {DateTime.UtcNow.ToString("O")}.");
|
||||||
|
AuthMigrationChanged?.Invoke(this, new AuthMigrationEventArgs(trace));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeferAuthMigration(TimeSpan deferred, string trace)
|
||||||
|
{
|
||||||
|
_allowAuthMigration.Reset();
|
||||||
|
|
||||||
|
// defer migration for a while
|
||||||
|
lock (_authMigrationLock)
|
||||||
|
{
|
||||||
|
_deferredAuthMigrationTime = DateTime.UtcNow.Add(deferred);
|
||||||
|
}
|
||||||
|
|
||||||
|
_trace.Info($"Disabled auth migration until {_deferredAuthMigrationTime.ToString("O")}.");
|
||||||
|
AuthMigrationChanged?.Invoke(this, new AuthMigrationEventArgs(trace));
|
||||||
|
}
|
||||||
|
|
||||||
public void LoadDefaultUserAgents()
|
public void LoadDefaultUserAgents()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(WebProxy.HttpProxyAddress) && string.IsNullOrEmpty(WebProxy.HttpsProxyAddress))
|
if (string.IsNullOrEmpty(WebProxy.HttpProxyAddress) && string.IsNullOrEmpty(WebProxy.HttpsProxyAddress))
|
||||||
@@ -227,6 +307,36 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
_userAgents.Add(new ProductInfoHeaderValue("ClientId", clientId));
|
_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);
|
var runnerFile = GetConfigFile(WellKnownConfigFile.Runner);
|
||||||
@@ -549,6 +659,18 @@ namespace GitHub.Runner.Common
|
|||||||
_loadContext.Unloading -= LoadContext_Unloading;
|
_loadContext.Unloading -= LoadContext_Unloading;
|
||||||
_loadContext = null;
|
_loadContext = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_authMigrationAutoReenableTask != null)
|
||||||
|
{
|
||||||
|
_authMigrationAutoReenableTaskCancellationTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_authMigrationAutoReenableTaskCancellationTokenSource != null)
|
||||||
|
{
|
||||||
|
_authMigrationAutoReenableTaskCancellationTokenSource?.Dispose();
|
||||||
|
_authMigrationAutoReenableTaskCancellationTokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
_httpTraceSubscription?.Dispose();
|
_httpTraceSubscription?.Dispose();
|
||||||
_diagListenerSubscription?.Dispose();
|
_diagListenerSubscription?.Dispose();
|
||||||
_traceManager?.Dispose();
|
_traceManager?.Dispose();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
void InitializeLaunchClient(Uri uri, string token);
|
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
|
public sealed class LaunchServer : RunnerService, ILaunchServer
|
||||||
@@ -42,12 +42,16 @@ namespace GitHub.Runner.Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList,
|
public Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors)
|
||||||
{
|
{
|
||||||
if (_launchClient != null)
|
if (_launchClient != null)
|
||||||
{
|
{
|
||||||
return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList,
|
if (!displayHelpfulActionsDownloadErrors)
|
||||||
cancellationToken: cancellationToken);
|
{
|
||||||
|
return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
return _launchClient.GetResolveActionsDownloadInfoAsyncV2(planId, jobId, actionReferenceList, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException("Launch client is not initialized.");
|
throw new InvalidOperationException("Launch client is not initialized.");
|
||||||
|
|||||||
@@ -32,18 +32,6 @@ namespace GitHub.Runner.Common
|
|||||||
string billingOwnerId,
|
string billingOwnerId,
|
||||||
CancellationToken token);
|
CancellationToken token);
|
||||||
|
|
||||||
Task CompleteJob2Async(
|
|
||||||
Guid planId,
|
|
||||||
Guid jobId,
|
|
||||||
TaskResult result,
|
|
||||||
Dictionary<String, VariableValue> outputs,
|
|
||||||
IList<StepResult> stepResults,
|
|
||||||
IList<Annotation> jobAnnotations,
|
|
||||||
string environmentUrl,
|
|
||||||
IList<Telemetry> telemetry,
|
|
||||||
string billingOwnerId,
|
|
||||||
CancellationToken token);
|
|
||||||
|
|
||||||
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +70,6 @@ namespace GitHub.Runner.Common
|
|||||||
ex is not TaskOrchestrationJobUnprocessableException); // HTTP status 422
|
ex is not TaskOrchestrationJobUnprocessableException); // HTTP status 422
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy will be deleted when SkipRetryCompleteJobUponKnownErrors is cleaned up
|
|
||||||
public Task CompleteJobAsync(
|
public Task CompleteJobAsync(
|
||||||
Guid planId,
|
Guid planId,
|
||||||
Guid jobId,
|
Guid jobId,
|
||||||
@@ -94,23 +81,6 @@ namespace GitHub.Runner.Common
|
|||||||
IList<Telemetry> telemetry,
|
IList<Telemetry> telemetry,
|
||||||
string billingOwnerId,
|
string billingOwnerId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
|
||||||
CheckConnection();
|
|
||||||
return RetryRequest(
|
|
||||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, environmentUrl, telemetry, billingOwnerId, cancellationToken), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task CompleteJob2Async(
|
|
||||||
Guid planId,
|
|
||||||
Guid jobId,
|
|
||||||
TaskResult result,
|
|
||||||
Dictionary<String, VariableValue> outputs,
|
|
||||||
IList<StepResult> stepResults,
|
|
||||||
IList<Annotation> jobAnnotations,
|
|
||||||
string environmentUrl,
|
|
||||||
IList<Telemetry> telemetry,
|
|
||||||
string billingOwnerId,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return RetryRequest(
|
return RetryRequest(
|
||||||
@@ -124,7 +94,9 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return RetryRequest<RenewJobResponse>(
|
return RetryRequest<RenewJobResponse>(
|
||||||
async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken);
|
async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken,
|
||||||
|
shouldRetry: ex =>
|
||||||
|
ex is not TaskOrchestrationJobNotFoundException); // HTTP status 404
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> 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<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);
|
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)
|
public async Task<List<TaskAgent>> GetRunnerByNameAsync(string githubUrl, string githubToken, string agentName)
|
||||||
{
|
{
|
||||||
var githubApiUrl = "";
|
var githubApiUrl = $"{GetEntityUrl(githubUrl)}/runners?name={Uri.EscapeDataString(agentName)}";
|
||||||
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 runnersList = await RetryRequest<ListRunnersResponse>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
var runnersList = await RetryRequest<ListRunnersResponse>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
||||||
|
|
||||||
return runnersList.ToTaskAgents();
|
return runnersList.ToTaskAgents();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken)
|
public async Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken)
|
||||||
{
|
{
|
||||||
var githubApiUrl = "";
|
var githubApiUrl = $"{GetEntityUrl(githubUrl)}/runner-groups";
|
||||||
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 agentPools = await RetryRequest<RunnerGroupList>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
var agentPools = await RetryRequest<RunnerGroupList>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
||||||
|
|
||||||
return agentPools?.ToAgentPoolList();
|
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);
|
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)
|
private async Task<T> RetryRequest<T>(string githubApiUrl, string githubToken, RequestType requestType, int maxRetryAttemptsCount = 5, string errorMessage = null, StringContent body = null)
|
||||||
{
|
{
|
||||||
int retry = 0;
|
int retry = 0;
|
||||||
@@ -220,13 +125,22 @@ namespace GitHub.Runner.Common
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = null;
|
HttpResponseMessage response = null;
|
||||||
if (requestType == RequestType.Get)
|
switch (requestType)
|
||||||
{
|
{
|
||||||
response = await httpClient.GetAsync(githubApiUrl);
|
case RequestType.Get:
|
||||||
}
|
response = await httpClient.GetAsync(githubApiUrl);
|
||||||
else
|
break;
|
||||||
{
|
case RequestType.Post:
|
||||||
response = await httpClient.PostAsync(githubApiUrl, body);
|
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)
|
if (response != null)
|
||||||
@@ -261,5 +175,61 @@ namespace GitHub.Runner.Common
|
|||||||
await Task.Delay(backOff);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Util
|
namespace GitHub.Runner.Common.Util
|
||||||
{
|
{
|
||||||
public static class NodeUtil
|
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";
|
private const string _defaultNodeVersion = "node20";
|
||||||
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] { "node20" });
|
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] { "node20" });
|
||||||
public static string GetInternalNodeVersion()
|
public static string GetInternalNodeVersion()
|
||||||
@@ -18,5 +41,122 @@ namespace GitHub.Runner.Common.Util
|
|||||||
}
|
}
|
||||||
return _defaultNodeVersion;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,15 +26,31 @@ namespace GitHub.Runner.Listener
|
|||||||
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
|
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
|
||||||
private CancellationTokenSource _getMessagesTokenSource;
|
private CancellationTokenSource _getMessagesTokenSource;
|
||||||
private VssCredentials _creds;
|
private VssCredentials _creds;
|
||||||
|
private VssCredentials _credsV2;
|
||||||
private TaskAgentSession _session;
|
private TaskAgentSession _session;
|
||||||
private IRunnerServer _runnerServer;
|
private IRunnerServer _runnerServer;
|
||||||
private IBrokerServer _brokerServer;
|
private IBrokerServer _brokerServer;
|
||||||
|
private ICredentialManager _credMgr;
|
||||||
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
|
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
|
||||||
private bool _accessTokenRevoked = false;
|
private bool _accessTokenRevoked = false;
|
||||||
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
|
private readonly TimeSpan _sessionCreationRetryInterval = TimeSpan.FromSeconds(30);
|
||||||
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
|
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
|
||||||
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
|
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
|
||||||
|
private bool _needRefreshCredsV2 = false;
|
||||||
|
private bool _handlerInitialized = false;
|
||||||
|
private bool _isMigratedSettings = false;
|
||||||
|
private const int _maxMigratedSettingsRetries = 3;
|
||||||
|
private int _migratedSettingsRetryCount = 0;
|
||||||
|
|
||||||
|
public BrokerMessageListener()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public BrokerMessageListener(RunnerSettings settings, bool isMigratedSettings = false)
|
||||||
|
{
|
||||||
|
_settings = settings;
|
||||||
|
_isMigratedSettings = isMigratedSettings;
|
||||||
|
}
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
@@ -43,15 +59,29 @@ namespace GitHub.Runner.Listener
|
|||||||
_term = HostContext.GetService<ITerminal>();
|
_term = HostContext.GetService<ITerminal>();
|
||||||
_runnerServer = HostContext.GetService<IRunnerServer>();
|
_runnerServer = HostContext.GetService<IRunnerServer>();
|
||||||
_brokerServer = HostContext.GetService<IBrokerServer>();
|
_brokerServer = HostContext.GetService<IBrokerServer>();
|
||||||
|
_credMgr = HostContext.GetService<ICredentialManager>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
|
public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
|
|
||||||
// Settings
|
// Load settings if not provided through constructor
|
||||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
if (_settings == null)
|
||||||
_settings = configManager.LoadSettings();
|
{
|
||||||
|
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||||
|
_settings = configManager.LoadSettings();
|
||||||
|
Trace.Info("Settings loaded from config manager");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info("Using provided settings");
|
||||||
|
if (_isMigratedSettings)
|
||||||
|
{
|
||||||
|
Trace.Info("Using migrated settings from .runner_migrated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var serverUrlV2 = _settings.ServerUrlV2;
|
var serverUrlV2 = _settings.ServerUrlV2;
|
||||||
var serverUrl = _settings.ServerUrl;
|
var serverUrl = _settings.ServerUrl;
|
||||||
Trace.Info(_settings);
|
Trace.Info(_settings);
|
||||||
@@ -63,8 +93,7 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
// Create connection.
|
// Create connection.
|
||||||
Trace.Info("Loading Credentials");
|
Trace.Info("Loading Credentials");
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
_creds = _credMgr.LoadCredentials(allowAuthUrlV2: false);
|
||||||
_creds = credMgr.LoadCredentials();
|
|
||||||
|
|
||||||
var agent = new TaskAgentReference
|
var agent = new TaskAgentReference
|
||||||
{
|
{
|
||||||
@@ -87,7 +116,8 @@ namespace GitHub.Runner.Listener
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Trace.Info("Connecting to the Broker Server...");
|
Trace.Info("Connecting to the Broker Server...");
|
||||||
await _brokerServer.ConnectAsync(new Uri(serverUrlV2), _creds);
|
_credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
|
||||||
|
await _brokerServer.ConnectAsync(new Uri(serverUrlV2), _credsV2);
|
||||||
Trace.Info("VssConnection created");
|
Trace.Info("VssConnection created");
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(serverUrl) &&
|
if (!string.IsNullOrEmpty(serverUrl) &&
|
||||||
@@ -112,6 +142,13 @@ namespace GitHub.Runner.Listener
|
|||||||
encounteringError = false;
|
encounteringError = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_handlerInitialized)
|
||||||
|
{
|
||||||
|
// Register event handler for auth migration state change
|
||||||
|
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
|
||||||
|
_handlerInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
return CreateSessionResult.Success;
|
return CreateSessionResult.Success;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
@@ -130,7 +167,22 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Error("Catch exception during create session.");
|
Trace.Error("Catch exception during create session.");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
|
|
||||||
if (ex is VssOAuthTokenRequestException vssOAuthEx && _creds.Federated is VssOAuthCredential vssOAuthCred)
|
// If using migrated settings, limit the number of retries before returning failure
|
||||||
|
if (_isMigratedSettings)
|
||||||
|
{
|
||||||
|
_migratedSettingsRetryCount++;
|
||||||
|
Trace.Warning($"Migrated settings retry {_migratedSettingsRetryCount} of {_maxMigratedSettingsRetries}");
|
||||||
|
|
||||||
|
if (_migratedSettingsRetryCount >= _maxMigratedSettingsRetries)
|
||||||
|
{
|
||||||
|
Trace.Warning("Reached maximum retry attempts for migrated settings. Returning failure to try default settings.");
|
||||||
|
return CreateSessionResult.Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HostContext.AllowAuthMigration &&
|
||||||
|
ex is VssOAuthTokenRequestException vssOAuthEx &&
|
||||||
|
_credsV2.Federated is VssOAuthCredential vssOAuthCred)
|
||||||
{
|
{
|
||||||
// "invalid_client" means the runner registration has been deleted from the server.
|
// "invalid_client" means the runner registration has been deleted from the server.
|
||||||
if (string.Equals(vssOAuthEx.Error, "invalid_client", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(vssOAuthEx.Error, "invalid_client", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -151,7 +203,8 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSessionCreationExceptionRetriable(ex))
|
if (!HostContext.AllowAuthMigration &&
|
||||||
|
!IsSessionCreationExceptionRetriable(ex))
|
||||||
{
|
{
|
||||||
_term.WriteError($"Failed to create session. {ex.Message}");
|
_term.WriteError($"Failed to create session. {ex.Message}");
|
||||||
if (ex is TaskAgentSessionConflictException)
|
if (ex is TaskAgentSessionConflictException)
|
||||||
@@ -161,6 +214,12 @@ namespace GitHub.Runner.Listener
|
|||||||
return CreateSessionResult.Failure;
|
return CreateSessionResult.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HostContext.AllowAuthMigration)
|
||||||
|
{
|
||||||
|
Trace.Info("Disable migration mode for 60 minutes.");
|
||||||
|
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Session creation failed with exception: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
if (!encounteringError) //print the message only on the first error
|
if (!encounteringError) //print the message only on the first error
|
||||||
{
|
{
|
||||||
_term.WriteError($"{DateTime.UtcNow:u}: Runner connect error: {ex.Message}. Retrying until reconnected.");
|
_term.WriteError($"{DateTime.UtcNow:u}: Runner connect error: {ex.Message}. Retrying until reconnected.");
|
||||||
@@ -177,6 +236,11 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
if (_session != null && _session.SessionId != Guid.Empty)
|
if (_session != null && _session.SessionId != Guid.Empty)
|
||||||
{
|
{
|
||||||
|
if (_handlerInitialized)
|
||||||
|
{
|
||||||
|
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_accessTokenRevoked)
|
if (!_accessTokenRevoked)
|
||||||
{
|
{
|
||||||
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
||||||
@@ -219,6 +283,13 @@ namespace GitHub.Runner.Listener
|
|||||||
_getMessagesTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
|
_getMessagesTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (_needRefreshCredsV2)
|
||||||
|
{
|
||||||
|
Trace.Info("Refreshing broker connection.");
|
||||||
|
await RefreshBrokerConnectionAsync();
|
||||||
|
_needRefreshCredsV2 = false;
|
||||||
|
}
|
||||||
|
|
||||||
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
|
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
|
||||||
runnerStatus,
|
runnerStatus,
|
||||||
BuildConstants.RunnerPackage.Version,
|
BuildConstants.RunnerPackage.Version,
|
||||||
@@ -254,11 +325,11 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Info("Hosted runner has been deprovisioned.");
|
Trace.Info("Hosted runner has been deprovisioned.");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (AccessDeniedException e) when (e.ErrorCode == 1)
|
catch (AccessDeniedException e) when (e.ErrorCode == 1 && !HostContext.AllowAuthMigration)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (RunnerNotFoundException)
|
catch (RunnerNotFoundException) when (!HostContext.AllowAuthMigration)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -267,7 +338,8 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Error("Catch exception during get next message.");
|
Trace.Error("Catch exception during get next message.");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
|
|
||||||
if (!IsGetNextMessageExceptionRetriable(ex))
|
if (!HostContext.AllowAuthMigration &&
|
||||||
|
!IsGetNextMessageExceptionRetriable(ex))
|
||||||
{
|
{
|
||||||
throw new NonRetryableException("Get next message failed with non-retryable error.", ex);
|
throw new NonRetryableException("Get next message failed with non-retryable error.", ex);
|
||||||
}
|
}
|
||||||
@@ -298,6 +370,12 @@ namespace GitHub.Runner.Listener
|
|||||||
encounteringError = true;
|
encounteringError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HostContext.AllowAuthMigration)
|
||||||
|
{
|
||||||
|
Trace.Info("Disable migration mode for 60 minutes.");
|
||||||
|
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Get next message failed with exception: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
// re-create VssConnection before next retry
|
// re-create VssConnection before next retry
|
||||||
await RefreshBrokerConnectionAsync();
|
await RefreshBrokerConnectionAsync();
|
||||||
|
|
||||||
@@ -329,7 +407,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshListenerTokenAsync(CancellationToken cancellationToken)
|
public async Task RefreshListenerTokenAsync()
|
||||||
{
|
{
|
||||||
await RefreshBrokerConnectionAsync();
|
await RefreshBrokerConnectionAsync();
|
||||||
}
|
}
|
||||||
@@ -432,17 +510,16 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
private async Task RefreshBrokerConnectionAsync()
|
private async Task RefreshBrokerConnectionAsync()
|
||||||
{
|
{
|
||||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
Trace.Info("Reload credentials.");
|
||||||
_settings = configManager.LoadSettings();
|
_credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
|
||||||
|
await _brokerServer.ConnectAsync(new Uri(_settings.ServerUrlV2), _credsV2);
|
||||||
|
Trace.Info("Connection to Broker Server recreated.");
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_settings.ServerUrlV2))
|
private void HandleAuthMigrationChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("ServerUrlV2 is not set");
|
Trace.Info($"Auth migration changed. Current allow auth migration state: {HostContext.AllowAuthMigration}");
|
||||||
}
|
_needRefreshCredsV2 = true;
|
||||||
|
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
|
||||||
VssCredentials creds = credMgr.LoadCredentials();
|
|
||||||
await _brokerServer.ConnectAsync(new Uri(_settings.ServerUrlV2), creds);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
Task UnconfigureAsync(CommandSettings command);
|
Task UnconfigureAsync(CommandSettings command);
|
||||||
void DeleteLocalRunnerConfig();
|
void DeleteLocalRunnerConfig();
|
||||||
RunnerSettings LoadSettings();
|
RunnerSettings LoadSettings();
|
||||||
|
RunnerSettings LoadMigratedSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ConfigurationManager : RunnerService, IConfigurationManager
|
public sealed class ConfigurationManager : RunnerService, IConfigurationManager
|
||||||
@@ -66,6 +67,22 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RunnerSettings LoadMigratedSettings()
|
||||||
|
{
|
||||||
|
Trace.Info(nameof(LoadMigratedSettings));
|
||||||
|
|
||||||
|
// Check if migrated settings file exists
|
||||||
|
if (!_store.IsMigratedConfigured())
|
||||||
|
{
|
||||||
|
throw new NonRetryableException("No migrated configuration found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RunnerSettings settings = _store.GetMigratedSettings();
|
||||||
|
Trace.Info("Migrated Settings Loaded");
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ConfigureAsync(CommandSettings command)
|
public async Task ConfigureAsync(CommandSettings command)
|
||||||
{
|
{
|
||||||
_term.WriteLine();
|
_term.WriteLine();
|
||||||
@@ -127,7 +144,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
runnerSettings.ServerUrl = inputUrl;
|
runnerSettings.ServerUrl = inputUrl;
|
||||||
// Get the credentials
|
// Get the credentials
|
||||||
credProvider = GetCredentialProvider(command, runnerSettings.ServerUrl);
|
credProvider = GetCredentialProvider(command, runnerSettings.ServerUrl);
|
||||||
creds = credProvider.GetVssCredentials(HostContext);
|
creds = credProvider.GetVssCredentials(HostContext, allowAuthUrlV2: false);
|
||||||
Trace.Info("legacy vss cred retrieved");
|
Trace.Info("legacy vss cred retrieved");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -366,10 +383,18 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
{
|
{
|
||||||
{ "clientId", agent.Authorization.ClientId.ToString("D") },
|
{ "clientId", agent.Authorization.ClientId.ToString("D") },
|
||||||
{ "authorizationUrl", agent.Authorization.AuthorizationUrl.AbsoluteUri },
|
{ "authorizationUrl", agent.Authorization.AuthorizationUrl.AbsoluteUri },
|
||||||
{ "requireFipsCryptography", agent.Properties.GetValue("RequireFipsCryptography", false).ToString() }
|
{ "requireFipsCryptography", agent.Properties.GetValue("RequireFipsCryptography", true).ToString() }
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
// Save the negotiated OAuth credential data
|
||||||
_store.SaveCredential(credentialData);
|
_store.SaveCredential(credentialData);
|
||||||
}
|
}
|
||||||
@@ -384,7 +409,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
if (!runnerSettings.UseV2Flow)
|
if (!runnerSettings.UseV2Flow)
|
||||||
{
|
{
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||||
VssCredentials credential = credMgr.LoadCredentials();
|
VssCredentials credential = credMgr.LoadCredentials(allowAuthUrlV2: false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
|
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
|
||||||
@@ -512,41 +537,50 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
if (isConfigured && hasCredentials)
|
if (isConfigured && hasCredentials)
|
||||||
{
|
{
|
||||||
RunnerSettings settings = _store.GetSettings();
|
RunnerSettings settings = _store.GetSettings();
|
||||||
var credentialManager = HostContext.GetService<ICredentialManager>();
|
|
||||||
|
|
||||||
// Get the credentials
|
if (settings.UseV2Flow)
|
||||||
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
|
|
||||||
{
|
{
|
||||||
var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove");
|
var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove");
|
||||||
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, deletionToken, Constants.RunnerEvent.Remove);
|
await _dotcomServer.DeleteRunnerAsync(settings.GitHubUrl, deletionToken, settings.AgentId);
|
||||||
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
|
else
|
||||||
{
|
{
|
||||||
await _runnerServer.DeleteAgentAsync(settings.AgentId);
|
var credentialManager = HostContext.GetService<ICredentialManager>();
|
||||||
|
|
||||||
_term.WriteLine();
|
// Get the credentials
|
||||||
_term.WriteSuccessMessage("Runner removed successfully");
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
public interface ICredentialManager : IRunnerService
|
public interface ICredentialManager : IRunnerService
|
||||||
{
|
{
|
||||||
ICredentialProvider GetCredentialProvider(string credType);
|
ICredentialProvider GetCredentialProvider(string credType);
|
||||||
VssCredentials LoadCredentials();
|
VssCredentials LoadCredentials(bool allowAuthUrlV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CredentialManager : RunnerService, ICredentialManager
|
public class CredentialManager : RunnerService, ICredentialManager
|
||||||
@@ -40,7 +40,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VssCredentials LoadCredentials()
|
public VssCredentials LoadCredentials(bool allowAuthUrlV2)
|
||||||
{
|
{
|
||||||
IConfigurationStore store = HostContext.GetService<IConfigurationStore>();
|
IConfigurationStore store = HostContext.GetService<IConfigurationStore>();
|
||||||
|
|
||||||
@@ -51,21 +51,16 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
CredentialData credData = store.GetCredentials();
|
CredentialData credData = store.GetCredentials();
|
||||||
var migratedCred = store.GetMigratedCredentials();
|
var migratedCred = store.GetMigratedCredentials();
|
||||||
if (migratedCred != null)
|
if (migratedCred != null &&
|
||||||
|
migratedCred.Scheme == Constants.Configuration.OAuth)
|
||||||
{
|
{
|
||||||
credData = migratedCred;
|
credData = migratedCred;
|
||||||
|
|
||||||
// Re-write .credentials with Token URL
|
|
||||||
store.SaveCredential(credData);
|
|
||||||
|
|
||||||
// Delete .credentials_migrated
|
|
||||||
store.DeleteMigratedCredential();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ICredentialProvider credProv = GetCredentialProvider(credData.Scheme);
|
ICredentialProvider credProv = GetCredentialProvider(credData.Scheme);
|
||||||
credProv.CredentialData = credData;
|
credProv.CredentialData = credData;
|
||||||
|
|
||||||
VssCredentials creds = credProv.GetVssCredentials(HostContext);
|
VssCredentials creds = credProv.GetVssCredentials(HostContext, allowAuthUrlV2);
|
||||||
|
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.OAuth;
|
using GitHub.Services.OAuth;
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener.Configuration
|
namespace GitHub.Runner.Listener.Configuration
|
||||||
@@ -10,7 +10,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
{
|
{
|
||||||
Boolean RequireInteractive { get; }
|
Boolean RequireInteractive { get; }
|
||||||
CredentialData CredentialData { get; set; }
|
CredentialData CredentialData { get; set; }
|
||||||
VssCredentials GetVssCredentials(IHostContext context);
|
VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2);
|
||||||
void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
|
void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
public virtual Boolean RequireInteractive => false;
|
public virtual Boolean RequireInteractive => false;
|
||||||
public CredentialData CredentialData { get; set; }
|
public CredentialData CredentialData { get; set; }
|
||||||
|
|
||||||
public abstract VssCredentials GetVssCredentials(IHostContext context);
|
public abstract VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2);
|
||||||
public abstract void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
|
public abstract void EnsureCredential(IHostContext context, CommandSettings command, string serverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
{
|
{
|
||||||
public OAuthAccessTokenCredential() : base(Constants.Configuration.OAuthAccessToken) { }
|
public OAuthAccessTokenCredential() : base(Constants.Configuration.OAuthAccessToken) { }
|
||||||
|
|
||||||
public override VssCredentials GetVssCredentials(IHostContext context)
|
public override VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2)
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(context, nameof(context));
|
ArgUtil.NotNull(context, nameof(context));
|
||||||
Tracing trace = context.GetTrace(nameof(OAuthAccessTokenCredential));
|
Tracing trace = context.GetTrace(nameof(OAuthAccessTokenCredential));
|
||||||
|
|||||||
@@ -22,10 +22,18 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
// Nothing to verify here
|
// Nothing to verify here
|
||||||
}
|
}
|
||||||
|
|
||||||
public override VssCredentials GetVssCredentials(IHostContext context)
|
public override VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2)
|
||||||
{
|
{
|
||||||
var clientId = this.CredentialData.Data.GetValueOrDefault("clientId", null);
|
var clientId = this.CredentialData.Data.GetValueOrDefault("clientId", null);
|
||||||
var authorizationUrl = this.CredentialData.Data.GetValueOrDefault("authorizationUrl", null);
|
var authorizationUrl = this.CredentialData.Data.GetValueOrDefault("authorizationUrl", null);
|
||||||
|
var authorizationUrlV2 = this.CredentialData.Data.GetValueOrDefault("authorizationUrlV2", null);
|
||||||
|
|
||||||
|
if (allowAuthUrlV2 &&
|
||||||
|
!string.IsNullOrEmpty(authorizationUrlV2) &&
|
||||||
|
context.AllowAuthMigration)
|
||||||
|
{
|
||||||
|
authorizationUrl = authorizationUrlV2;
|
||||||
|
}
|
||||||
|
|
||||||
// For back compat with .credential file that doesn't has 'oauthEndpointUrl' section
|
// For back compat with .credential file that doesn't has 'oauthEndpointUrl' section
|
||||||
var oauthEndpointUrl = this.CredentialData.Data.GetValueOrDefault("oauthEndpointUrl", authorizationUrl);
|
var oauthEndpointUrl = this.CredentialData.Data.GetValueOrDefault("oauthEndpointUrl", authorizationUrl);
|
||||||
|
|||||||
@@ -110,7 +110,12 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
var jwt = JsonWebToken.Create(accessToken);
|
var jwt = JsonWebToken.Create(accessToken);
|
||||||
var claims = jwt.ExtractClaims();
|
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))
|
if (!string.IsNullOrEmpty(orchestrationId))
|
||||||
{
|
{
|
||||||
Trace.Info($"Pull OrchestrationId {orchestrationId} from JWT claims");
|
Trace.Info($"Pull OrchestrationId {orchestrationId} from JWT claims");
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace GitHub.Runner.Listener
|
|||||||
Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token);
|
Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token);
|
||||||
Task DeleteMessageAsync(TaskAgentMessage message);
|
Task DeleteMessageAsync(TaskAgentMessage message);
|
||||||
|
|
||||||
Task RefreshListenerTokenAsync(CancellationToken token);
|
Task RefreshListenerTokenAsync();
|
||||||
void OnJobStatus(object sender, JobStatusEventArgs e);
|
void OnJobStatus(object sender, JobStatusEventArgs e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ namespace GitHub.Runner.Listener
|
|||||||
private ITerminal _term;
|
private ITerminal _term;
|
||||||
private IRunnerServer _runnerServer;
|
private IRunnerServer _runnerServer;
|
||||||
private IBrokerServer _brokerServer;
|
private IBrokerServer _brokerServer;
|
||||||
|
private ICredentialManager _credMgr;
|
||||||
private TaskAgentSession _session;
|
private TaskAgentSession _session;
|
||||||
private TimeSpan _getNextMessageRetryInterval;
|
private TimeSpan _getNextMessageRetryInterval;
|
||||||
private bool _accessTokenRevoked = false;
|
private bool _accessTokenRevoked = false;
|
||||||
@@ -54,8 +55,9 @@ namespace GitHub.Runner.Listener
|
|||||||
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
|
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
|
||||||
private CancellationTokenSource _getMessagesTokenSource;
|
private CancellationTokenSource _getMessagesTokenSource;
|
||||||
private VssCredentials _creds;
|
private VssCredentials _creds;
|
||||||
|
private VssCredentials _credsV2;
|
||||||
private bool _isBrokerSession = false;
|
private bool _needRefreshCredsV2 = false;
|
||||||
|
private bool _handlerInitialized = false;
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
@@ -64,6 +66,7 @@ namespace GitHub.Runner.Listener
|
|||||||
_term = HostContext.GetService<ITerminal>();
|
_term = HostContext.GetService<ITerminal>();
|
||||||
_runnerServer = HostContext.GetService<IRunnerServer>();
|
_runnerServer = HostContext.GetService<IRunnerServer>();
|
||||||
_brokerServer = hostContext.GetService<IBrokerServer>();
|
_brokerServer = hostContext.GetService<IBrokerServer>();
|
||||||
|
_credMgr = hostContext.GetService<ICredentialManager>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
|
public async Task<CreateSessionResult> CreateSessionAsync(CancellationToken token)
|
||||||
@@ -78,8 +81,7 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
// Create connection.
|
// Create connection.
|
||||||
Trace.Info("Loading Credentials");
|
Trace.Info("Loading Credentials");
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
_creds = _credMgr.LoadCredentials(allowAuthUrlV2: false);
|
||||||
_creds = credMgr.LoadCredentials();
|
|
||||||
|
|
||||||
var agent = new TaskAgentReference
|
var agent = new TaskAgentReference
|
||||||
{
|
{
|
||||||
@@ -113,16 +115,6 @@ namespace GitHub.Runner.Listener
|
|||||||
_settings.PoolId,
|
_settings.PoolId,
|
||||||
taskAgentSession,
|
taskAgentSession,
|
||||||
token);
|
token);
|
||||||
|
|
||||||
if (_session.BrokerMigrationMessage != null)
|
|
||||||
{
|
|
||||||
Trace.Info("Runner session is in migration mode: Creating Broker session with BrokerBaseUrl: {0}", _session.BrokerMigrationMessage.BrokerBaseUrl);
|
|
||||||
|
|
||||||
await _brokerServer.UpdateConnectionIfNeeded(_session.BrokerMigrationMessage.BrokerBaseUrl, _creds);
|
|
||||||
_session = await _brokerServer.CreateSessionAsync(taskAgentSession, token);
|
|
||||||
_isBrokerSession = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace.Info($"Session created.");
|
Trace.Info($"Session created.");
|
||||||
if (encounteringError)
|
if (encounteringError)
|
||||||
{
|
{
|
||||||
@@ -131,6 +123,13 @@ namespace GitHub.Runner.Listener
|
|||||||
encounteringError = false;
|
encounteringError = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_handlerInitialized)
|
||||||
|
{
|
||||||
|
Trace.Info("Registering AuthMigrationChanged event handler.");
|
||||||
|
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
|
||||||
|
_handlerInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
return CreateSessionResult.Success;
|
return CreateSessionResult.Success;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||||
@@ -196,16 +195,16 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
if (_session != null && _session.SessionId != Guid.Empty)
|
if (_session != null && _session.SessionId != Guid.Empty)
|
||||||
{
|
{
|
||||||
|
if (_handlerInitialized)
|
||||||
|
{
|
||||||
|
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_accessTokenRevoked)
|
if (!_accessTokenRevoked)
|
||||||
{
|
{
|
||||||
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
using (var ts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
||||||
{
|
{
|
||||||
await _runnerServer.DeleteAgentSessionAsync(_settings.PoolId, _session.SessionId, ts.Token);
|
await _runnerServer.DeleteAgentSessionAsync(_settings.PoolId, _session.SessionId, ts.Token);
|
||||||
|
|
||||||
if (_isBrokerSession)
|
|
||||||
{
|
|
||||||
await _brokerServer.DeleteSessionAsync(ts.Token);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -261,12 +260,19 @@ namespace GitHub.Runner.Listener
|
|||||||
// Decrypt the message body if the session is using encryption
|
// Decrypt the message body if the session is using encryption
|
||||||
message = DecryptMessage(message);
|
message = DecryptMessage(message);
|
||||||
|
|
||||||
|
|
||||||
if (message != null && message.MessageType == BrokerMigrationMessage.MessageType)
|
if (message != null && message.MessageType == BrokerMigrationMessage.MessageType)
|
||||||
{
|
{
|
||||||
var migrationMessage = JsonUtility.FromString<BrokerMigrationMessage>(message.Body);
|
var migrationMessage = JsonUtility.FromString<BrokerMigrationMessage>(message.Body);
|
||||||
|
|
||||||
await _brokerServer.UpdateConnectionIfNeeded(migrationMessage.BrokerBaseUrl, _creds);
|
_credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
|
||||||
|
await _brokerServer.UpdateConnectionIfNeeded(migrationMessage.BrokerBaseUrl, _credsV2);
|
||||||
|
if (_needRefreshCredsV2)
|
||||||
|
{
|
||||||
|
Trace.Info("Refreshing credentials for V2.");
|
||||||
|
await _brokerServer.ForceRefreshConnection(_credsV2);
|
||||||
|
_needRefreshCredsV2 = false;
|
||||||
|
}
|
||||||
|
|
||||||
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
|
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
|
||||||
runnerStatus,
|
runnerStatus,
|
||||||
BuildConstants.RunnerPackage.Version,
|
BuildConstants.RunnerPackage.Version,
|
||||||
@@ -309,11 +315,11 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Info("Hosted runner has been deprovisioned.");
|
Trace.Info("Hosted runner has been deprovisioned.");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (AccessDeniedException e) when (e.ErrorCode == 1)
|
catch (AccessDeniedException e) when (e.ErrorCode == 1 && !HostContext.AllowAuthMigration)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (RunnerNotFoundException)
|
catch (RunnerNotFoundException) when (!HostContext.AllowAuthMigration)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -322,12 +328,19 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Error("Catch exception during get next message.");
|
Trace.Error("Catch exception during get next message.");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
|
|
||||||
|
// clear out potential message for broker migration,
|
||||||
|
// in case the exception is thrown from get message from broker-listener.
|
||||||
|
message = null;
|
||||||
|
|
||||||
// don't retry if SkipSessionRecover = true, DT service will delete agent session to stop agent from taking more jobs.
|
// don't retry if SkipSessionRecover = true, DT service will delete agent session to stop agent from taking more jobs.
|
||||||
if (ex is TaskAgentSessionExpiredException && !_settings.SkipSessionRecover && (await CreateSessionAsync(token) == CreateSessionResult.Success))
|
if (!HostContext.AllowAuthMigration &&
|
||||||
|
ex is TaskAgentSessionExpiredException &&
|
||||||
|
!_settings.SkipSessionRecover && (await CreateSessionAsync(token) == CreateSessionResult.Success))
|
||||||
{
|
{
|
||||||
Trace.Info($"{nameof(TaskAgentSessionExpiredException)} received, recovered by recreate session.");
|
Trace.Info($"{nameof(TaskAgentSessionExpiredException)} received, recovered by recreate session.");
|
||||||
}
|
}
|
||||||
else if (!IsGetNextMessageExceptionRetriable(ex))
|
else if (!HostContext.AllowAuthMigration &&
|
||||||
|
!IsGetNextMessageExceptionRetriable(ex))
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -354,6 +367,12 @@ namespace GitHub.Runner.Listener
|
|||||||
encounteringError = true;
|
encounteringError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HostContext.AllowAuthMigration)
|
||||||
|
{
|
||||||
|
Trace.Info("Disable migration mode for 60 minutes.");
|
||||||
|
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Get next message failed with exception: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
// re-create VssConnection before next retry
|
// re-create VssConnection before next retry
|
||||||
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
|
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
|
||||||
|
|
||||||
@@ -411,10 +430,11 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshListenerTokenAsync(CancellationToken cancellationToken)
|
public async Task RefreshListenerTokenAsync()
|
||||||
{
|
{
|
||||||
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
|
await _runnerServer.RefreshConnectionAsync(RunnerConnectionType.MessageQueue, TimeSpan.FromSeconds(60));
|
||||||
await _brokerServer.ForceRefreshConnection(_creds);
|
_credsV2 = _credMgr.LoadCredentials(allowAuthUrlV2: true);
|
||||||
|
await _brokerServer.ForceRefreshConnection(_credsV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TaskAgentMessage DecryptMessage(TaskAgentMessage message)
|
private TaskAgentMessage DecryptMessage(TaskAgentMessage message)
|
||||||
@@ -545,5 +565,11 @@ namespace GitHub.Runner.Listener
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleAuthMigrationChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Trace.Info($"Auth migration changed. Current allow auth migration state: {HostContext.AllowAuthMigration}");
|
||||||
|
_needRefreshCredsV2 = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -14,7 +16,9 @@ using GitHub.Runner.Common.Util;
|
|||||||
using GitHub.Runner.Listener.Check;
|
using GitHub.Runner.Listener.Check;
|
||||||
using GitHub.Runner.Listener.Configuration;
|
using GitHub.Runner.Listener.Configuration;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Services.OAuth;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
|
using GitHub.Services.WebApi.Jwt;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener
|
namespace GitHub.Runner.Listener
|
||||||
@@ -31,6 +35,14 @@ namespace GitHub.Runner.Listener
|
|||||||
private ITerminal _term;
|
private ITerminal _term;
|
||||||
private bool _inConfigStage;
|
private bool _inConfigStage;
|
||||||
private ManualResetEvent _completedCommand = new(false);
|
private ManualResetEvent _completedCommand = new(false);
|
||||||
|
private readonly ConcurrentQueue<string> _authMigrationTelemetries = new();
|
||||||
|
private Task _authMigrationTelemetryTask;
|
||||||
|
private readonly object _authMigrationTelemetryLock = new();
|
||||||
|
private Task _authMigrationClaimsCheckTask;
|
||||||
|
private readonly object _authMigrationClaimsCheckLock = new();
|
||||||
|
private IRunnerServer _runnerServer;
|
||||||
|
private CancellationTokenSource _authMigrationTelemetryTokenSource = new();
|
||||||
|
private CancellationTokenSource _authMigrationClaimsCheckTokenSource = new();
|
||||||
|
|
||||||
// <summary>
|
// <summary>
|
||||||
// Helps avoid excessive calls to Run Service when encountering non-retriable errors from /acquirejob.
|
// Helps avoid excessive calls to Run Service when encountering non-retriable errors from /acquirejob.
|
||||||
@@ -51,6 +63,7 @@ namespace GitHub.Runner.Listener
|
|||||||
base.Initialize(hostContext);
|
base.Initialize(hostContext);
|
||||||
_term = HostContext.GetService<ITerminal>();
|
_term = HostContext.GetService<ITerminal>();
|
||||||
_acquireJobThrottler = HostContext.CreateService<IErrorThrottler>();
|
_acquireJobThrottler = HostContext.CreateService<IErrorThrottler>();
|
||||||
|
_runnerServer = HostContext.GetService<IRunnerServer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> ExecuteCommand(CommandSettings command)
|
public async Task<int> ExecuteCommand(CommandSettings command)
|
||||||
@@ -66,6 +79,8 @@ namespace GitHub.Runner.Listener
|
|||||||
//register a SIGTERM handler
|
//register a SIGTERM handler
|
||||||
HostContext.Unloading += Runner_Unloading;
|
HostContext.Unloading += Runner_Unloading;
|
||||||
|
|
||||||
|
HostContext.AuthMigrationChanged += HandleAuthMigrationChanged;
|
||||||
|
|
||||||
// TODO Unit test to cover this logic
|
// TODO Unit test to cover this logic
|
||||||
Trace.Info(nameof(ExecuteCommand));
|
Trace.Info(nameof(ExecuteCommand));
|
||||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||||
@@ -300,8 +315,17 @@ namespace GitHub.Runner.Listener
|
|||||||
_term.WriteLine("https://docs.github.com/en/actions/hosting-your-own-runners/autoscaling-with-self-hosted-runners#using-ephemeral-runners-for-autoscaling", ConsoleColor.Yellow);
|
_term.WriteLine("https://docs.github.com/en/actions/hosting-your-own-runners/autoscaling-with-self-hosted-runners#using-ephemeral-runners-for-autoscaling", ConsoleColor.Yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cred = store.GetCredentials();
|
||||||
|
if (cred != null &&
|
||||||
|
cred.Scheme == Constants.Configuration.OAuth &&
|
||||||
|
cred.Data.ContainsKey("EnableAuthMigrationByDefault"))
|
||||||
|
{
|
||||||
|
Trace.Info("Enable auth migration by default.");
|
||||||
|
HostContext.EnableAuthMigration("EnableAuthMigrationByDefault");
|
||||||
|
}
|
||||||
|
|
||||||
// Run the runner interactively or as service
|
// Run the runner interactively or as service
|
||||||
return await RunAsync(settings, command.RunOnce || settings.Ephemeral);
|
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -311,6 +335,9 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
_authMigrationClaimsCheckTokenSource?.Cancel();
|
||||||
|
_authMigrationTelemetryTokenSource?.Cancel();
|
||||||
|
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
|
||||||
_term.CancelKeyPress -= CtrlCHandler;
|
_term.CancelKeyPress -= CtrlCHandler;
|
||||||
HostContext.Unloading -= Runner_Unloading;
|
HostContext.Unloading -= Runner_Unloading;
|
||||||
_completedCommand.Set();
|
_completedCommand.Set();
|
||||||
@@ -360,12 +387,12 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IMessageListener GetMessageListener(RunnerSettings settings)
|
private IMessageListener GetMessageListener(RunnerSettings settings, bool isMigratedSettings = false)
|
||||||
{
|
{
|
||||||
if (settings.UseV2Flow)
|
if (settings.UseV2Flow)
|
||||||
{
|
{
|
||||||
Trace.Info($"Using BrokerMessageListener");
|
Trace.Info($"Using BrokerMessageListener");
|
||||||
var brokerListener = new BrokerMessageListener();
|
var brokerListener = new BrokerMessageListener(settings, isMigratedSettings);
|
||||||
brokerListener.Initialize(HostContext);
|
brokerListener.Initialize(HostContext);
|
||||||
return brokerListener;
|
return brokerListener;
|
||||||
}
|
}
|
||||||
@@ -379,15 +406,65 @@ namespace GitHub.Runner.Listener
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Trace.Info(nameof(RunAsync));
|
Trace.Info(nameof(RunAsync));
|
||||||
_listener = GetMessageListener(settings);
|
|
||||||
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
// First try using migrated settings if available
|
||||||
if (createSessionResult == CreateSessionResult.SessionConflict)
|
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||||
|
RunnerSettings migratedSettings = null;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
return Constants.Runner.ReturnCode.SessionConflict;
|
migratedSettings = configManager.LoadMigratedSettings();
|
||||||
|
Trace.Info("Loaded migrated settings from .runner_migrated file");
|
||||||
|
Trace.Info(migratedSettings);
|
||||||
}
|
}
|
||||||
else if (createSessionResult == CreateSessionResult.Failure)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return Constants.Runner.ReturnCode.TerminatedError;
|
// If migrated settings file doesn't exist or can't be loaded, we'll use the provided settings
|
||||||
|
Trace.Info($"Failed to load migrated settings: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool usedMigratedSettings = false;
|
||||||
|
|
||||||
|
if (migratedSettings != null)
|
||||||
|
{
|
||||||
|
// Try to create session with migrated settings first
|
||||||
|
Trace.Info("Attempting to create session using migrated settings");
|
||||||
|
_listener = GetMessageListener(migratedSettings, isMigratedSettings: true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
||||||
|
if (createSessionResult == CreateSessionResult.Success)
|
||||||
|
{
|
||||||
|
Trace.Info("Successfully created session with migrated settings");
|
||||||
|
settings = migratedSettings; // Use migrated settings for the rest of the process
|
||||||
|
usedMigratedSettings = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Warning($"Failed to create session with migrated settings: {createSessionResult}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Exception when creating session with migrated settings: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If migrated settings weren't used or session creation failed, use original settings
|
||||||
|
if (!usedMigratedSettings)
|
||||||
|
{
|
||||||
|
Trace.Info("Falling back to original .runner settings");
|
||||||
|
_listener = GetMessageListener(settings);
|
||||||
|
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
||||||
|
if (createSessionResult == CreateSessionResult.SessionConflict)
|
||||||
|
{
|
||||||
|
return Constants.Runner.ReturnCode.SessionConflict;
|
||||||
|
}
|
||||||
|
else if (createSessionResult == CreateSessionResult.Failure)
|
||||||
|
{
|
||||||
|
return Constants.Runner.ReturnCode.TerminatedError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HostContext.WritePerfCounter("SessionCreated");
|
HostContext.WritePerfCounter("SessionCreated");
|
||||||
@@ -401,6 +478,8 @@ namespace GitHub.Runner.Listener
|
|||||||
// Should we try to cleanup ephemeral runners
|
// Should we try to cleanup ephemeral runners
|
||||||
bool runOnceJobCompleted = false;
|
bool runOnceJobCompleted = false;
|
||||||
bool skipSessionDeletion = false;
|
bool skipSessionDeletion = false;
|
||||||
|
bool restartSession = false; // Flag to indicate session restart
|
||||||
|
bool restartSessionPending = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var notification = HostContext.GetService<IJobNotification>();
|
var notification = HostContext.GetService<IJobNotification>();
|
||||||
@@ -416,6 +495,15 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
while (!HostContext.RunnerShutdownToken.IsCancellationRequested)
|
while (!HostContext.RunnerShutdownToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
// Check if we need to restart the session and can do so (job dispatcher not busy)
|
||||||
|
if (restartSessionPending && !jobDispatcher.Busy)
|
||||||
|
{
|
||||||
|
Trace.Info("Pending session restart detected and job dispatcher is not busy. Restarting session now.");
|
||||||
|
messageQueueLoopTokenSource.Cancel();
|
||||||
|
restartSession = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
TaskAgentMessage message = null;
|
TaskAgentMessage message = null;
|
||||||
bool skipMessageDeletion = false;
|
bool skipMessageDeletion = false;
|
||||||
try
|
try
|
||||||
@@ -570,18 +658,18 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
// Create connection
|
// Create connection
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||||
var creds = credMgr.LoadCredentials();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(messageRef.RunServiceUrl))
|
if (string.IsNullOrEmpty(messageRef.RunServiceUrl))
|
||||||
{
|
{
|
||||||
|
var creds = credMgr.LoadCredentials(allowAuthUrlV2: false);
|
||||||
var actionsRunServer = HostContext.CreateService<IActionsRunServer>();
|
var actionsRunServer = HostContext.CreateService<IActionsRunServer>();
|
||||||
await actionsRunServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
|
await actionsRunServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
|
||||||
jobRequestMessage = await actionsRunServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
|
jobRequestMessage = await actionsRunServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var credsV2 = credMgr.LoadCredentials(allowAuthUrlV2: true);
|
||||||
var runServer = HostContext.CreateService<IRunServer>();
|
var runServer = HostContext.CreateService<IRunServer>();
|
||||||
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), creds);
|
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), credsV2);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageRef.BillingOwnerId, messageQueueLoopTokenSource.Token);
|
jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageRef.BillingOwnerId, messageQueueLoopTokenSource.Token);
|
||||||
@@ -599,6 +687,13 @@ namespace GitHub.Runner.Listener
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Trace.Error($"Caught exception from acquiring job message: {ex}");
|
Trace.Error($"Caught exception from acquiring job message: {ex}");
|
||||||
|
|
||||||
|
if (HostContext.AllowAuthMigration)
|
||||||
|
{
|
||||||
|
Trace.Info("Disable migration mode for 60 minutes.");
|
||||||
|
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Acquire job failed with exception: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -633,7 +728,7 @@ namespace GitHub.Runner.Listener
|
|||||||
else if (string.Equals(message.MessageType, TaskAgentMessageTypes.ForceTokenRefresh))
|
else if (string.Equals(message.MessageType, TaskAgentMessageTypes.ForceTokenRefresh))
|
||||||
{
|
{
|
||||||
Trace.Info("Received ForceTokenRefreshMessage");
|
Trace.Info("Received ForceTokenRefreshMessage");
|
||||||
await _listener.RefreshListenerTokenAsync(messageQueueLoopTokenSource.Token);
|
await _listener.RefreshListenerTokenAsync();
|
||||||
}
|
}
|
||||||
else if (string.Equals(message.MessageType, RunnerRefreshConfigMessage.MessageType))
|
else if (string.Equals(message.MessageType, RunnerRefreshConfigMessage.MessageType))
|
||||||
{
|
{
|
||||||
@@ -645,6 +740,17 @@ namespace GitHub.Runner.Listener
|
|||||||
configType: runnerRefreshConfigMessage.ConfigType,
|
configType: runnerRefreshConfigMessage.ConfigType,
|
||||||
serviceType: runnerRefreshConfigMessage.ServiceType,
|
serviceType: runnerRefreshConfigMessage.ServiceType,
|
||||||
configRefreshUrl: runnerRefreshConfigMessage.ConfigRefreshUrl);
|
configRefreshUrl: runnerRefreshConfigMessage.ConfigRefreshUrl);
|
||||||
|
|
||||||
|
// Set flag to schedule session restart if ConfigType is "runner"
|
||||||
|
if (string.Equals(runnerRefreshConfigMessage.ConfigType, "runner", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Trace.Info("Runner configuration was updated. Session restart has been scheduled");
|
||||||
|
restartSessionPending = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Trace.Info($"No session restart needed for config type: {runnerRefreshConfigMessage.ConfigType}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -699,10 +805,16 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
if (settings.Ephemeral && runOnceJobCompleted)
|
if (settings.Ephemeral && runOnceJobCompleted)
|
||||||
{
|
{
|
||||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
|
||||||
configManager.DeleteLocalRunnerConfig();
|
configManager.DeleteLocalRunnerConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After cleanup, check if we need to restart the session
|
||||||
|
if (restartSession)
|
||||||
|
{
|
||||||
|
Trace.Info("Restarting runner session after config update...");
|
||||||
|
return Constants.Runner.ReturnCode.RunnerConfigurationRefreshed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (TaskAgentAccessTokenExpiredException)
|
catch (TaskAgentAccessTokenExpiredException)
|
||||||
{
|
{
|
||||||
@@ -716,6 +828,220 @@ namespace GitHub.Runner.Listener
|
|||||||
return Constants.Runner.ReturnCode.Success;
|
return Constants.Runner.ReturnCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce)
|
||||||
|
{
|
||||||
|
int returnCode = Constants.Runner.ReturnCode.Success;
|
||||||
|
bool restart = false;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
restart = false;
|
||||||
|
returnCode = await RunAsync(settings, runOnce);
|
||||||
|
|
||||||
|
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
|
||||||
|
{
|
||||||
|
Trace.Info("Runner configuration was refreshed, restarting session...");
|
||||||
|
// Reload settings in case they changed
|
||||||
|
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||||
|
settings = configManager.LoadSettings();
|
||||||
|
restart = true;
|
||||||
|
}
|
||||||
|
} while (restart);
|
||||||
|
|
||||||
|
return returnCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleAuthMigrationChanged(object sender, AuthMigrationEventArgs e)
|
||||||
|
{
|
||||||
|
Trace.Verbose("Handle AuthMigrationChanged in Runner");
|
||||||
|
_authMigrationTelemetries.Enqueue($"{DateTime.UtcNow.ToString("O")}: {e.Trace}");
|
||||||
|
|
||||||
|
// only start the telemetry reporting task once auth migration is changed (enabled or disabled)
|
||||||
|
lock (_authMigrationTelemetryLock)
|
||||||
|
{
|
||||||
|
if (_authMigrationTelemetryTask == null)
|
||||||
|
{
|
||||||
|
_authMigrationTelemetryTask = ReportAuthMigrationTelemetryAsync(_authMigrationTelemetryTokenSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only start the claims check task once auth migration is changed (enabled or disabled)
|
||||||
|
lock (_authMigrationClaimsCheckLock)
|
||||||
|
{
|
||||||
|
if (_authMigrationClaimsCheckTask == null)
|
||||||
|
{
|
||||||
|
_authMigrationClaimsCheckTask = CheckOAuthTokenClaimsAsync(_authMigrationClaimsCheckTokenSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckOAuthTokenClaimsAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
string[] expectedClaims =
|
||||||
|
[
|
||||||
|
"owner_id",
|
||||||
|
"runner_id",
|
||||||
|
"runner_group_id",
|
||||||
|
"scale_set_id",
|
||||||
|
"is_ephemeral",
|
||||||
|
"labels"
|
||||||
|
];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await HostContext.Delay(TimeSpan.FromMinutes(100), token);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Ignore cancellation
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HostContext.AllowAuthMigration)
|
||||||
|
{
|
||||||
|
Trace.Info("Skip checking oauth token claims since auth migration is disabled.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baselineCred = credMgr.LoadCredentials(allowAuthUrlV2: false);
|
||||||
|
var authV2Cred = credMgr.LoadCredentials(allowAuthUrlV2: true);
|
||||||
|
|
||||||
|
if (!(baselineCred.Federated is VssOAuthCredential baselineVssOAuthCred) ||
|
||||||
|
!(authV2Cred.Federated is VssOAuthCredential vssOAuthCredV2) ||
|
||||||
|
baselineVssOAuthCred == null ||
|
||||||
|
vssOAuthCredV2 == null)
|
||||||
|
{
|
||||||
|
Trace.Info("Skip checking oauth token claims for non-oauth credentials");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(baselineVssOAuthCred.AuthorizationUrl.AbsoluteUri, vssOAuthCredV2.AuthorizationUrl.AbsoluteUri, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Trace.Info("Skip checking oauth token claims for same authorization url");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baselineProvider = baselineVssOAuthCred.GetTokenProvider(baselineVssOAuthCred.AuthorizationUrl);
|
||||||
|
var v2Provider = vssOAuthCredV2.GetTokenProvider(vssOAuthCredV2.AuthorizationUrl);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
||||||
|
using (var requestTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutTokenSource.Token))
|
||||||
|
{
|
||||||
|
var baselineToken = await baselineProvider.GetTokenAsync(null, requestTokenSource.Token);
|
||||||
|
var v2Token = await v2Provider.GetTokenAsync(null, requestTokenSource.Token);
|
||||||
|
if (baselineToken is VssOAuthAccessToken baselineAccessToken &&
|
||||||
|
v2Token is VssOAuthAccessToken v2AccessToken &&
|
||||||
|
!string.IsNullOrEmpty(baselineAccessToken.Value) &&
|
||||||
|
!string.IsNullOrEmpty(v2AccessToken.Value))
|
||||||
|
{
|
||||||
|
var baselineJwt = JsonWebToken.Create(baselineAccessToken.Value);
|
||||||
|
var baselineClaims = baselineJwt.ExtractClaims();
|
||||||
|
var v2Jwt = JsonWebToken.Create(v2AccessToken.Value);
|
||||||
|
var v2Claims = v2Jwt.ExtractClaims();
|
||||||
|
|
||||||
|
// Log extracted claims for debugging
|
||||||
|
Trace.Verbose($"Baseline token expected claims: {string.Join(", ", baselineClaims
|
||||||
|
.Where(c => expectedClaims.Contains(c.Type.ToLowerInvariant()))
|
||||||
|
.Select(c => $"{c.Type}:{c.Value}"))}");
|
||||||
|
Trace.Verbose($"V2 token expected claims: {string.Join(", ", v2Claims
|
||||||
|
.Where(c => expectedClaims.Contains(c.Type.ToLowerInvariant()))
|
||||||
|
.Select(c => $"{c.Type}:{c.Value}"))}");
|
||||||
|
|
||||||
|
foreach (var claim in expectedClaims)
|
||||||
|
{
|
||||||
|
// if baseline has the claim, v2 should have it too with exactly same value.
|
||||||
|
if (baselineClaims.FirstOrDefault(c => c.Type.ToLowerInvariant() == claim) is Claim baselineClaim &&
|
||||||
|
!string.IsNullOrEmpty(baselineClaim?.Value))
|
||||||
|
{
|
||||||
|
var v2Claim = v2Claims.FirstOrDefault(c => c.Type.ToLowerInvariant() == claim);
|
||||||
|
if (v2Claim?.Value != baselineClaim.Value)
|
||||||
|
{
|
||||||
|
Trace.Info($"Token Claim mismatch between two issuers. Expected: {baselineClaim.Type}:{baselineClaim.Value}. Actual: {v2Claim?.Type ?? "Empty"}:{v2Claim?.Value ?? "Empty"}");
|
||||||
|
HostContext.DeferAuthMigration(TimeSpan.FromMinutes(60), $"Expected claim {baselineClaim.Type}:{baselineClaim.Value} does not match {v2Claim?.Type ?? "Empty"}:{v2Claim?.Value ?? "Empty"}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Info("OAuth token claims check passed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error("Failed to fetch and check OAuth token claims.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error("Failed to check OAuth token claims in background.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReportAuthMigrationTelemetryAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||||
|
var runnerSettings = configManager.LoadSettings();
|
||||||
|
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await HostContext.Delay(TimeSpan.FromSeconds(60), token);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Ignore cancellation
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Verbose("Checking for auth migration telemetry to report");
|
||||||
|
while (_authMigrationTelemetries.TryDequeue(out var telemetry))
|
||||||
|
{
|
||||||
|
Trace.Verbose($"Reporting auth migration telemetry: {telemetry}");
|
||||||
|
if (runnerSettings != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
||||||
|
{
|
||||||
|
await _runnerServer.UpdateAgentUpdateStateAsync(runnerSettings.PoolId, runnerSettings.AgentId, "RefreshConfig", telemetry, tokenSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error("Failed to report auth migration telemetry.");
|
||||||
|
Trace.Error(ex);
|
||||||
|
_authMigrationTelemetries.Enqueue(telemetry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await HostContext.Delay(TimeSpan.FromSeconds(10), token);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Ignore cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void PrintUsage(CommandSettings command)
|
private void PrintUsage(CommandSettings command)
|
||||||
{
|
{
|
||||||
string separator;
|
string separator;
|
||||||
|
|||||||
@@ -197,11 +197,31 @@ namespace GitHub.Runner.Listener
|
|||||||
await ReportTelemetryAsync($"Credential clientId in refreshed config '{refreshedClientId ?? "Empty"}' does not match the current credential clientId '{clientId}'.");
|
await ReportTelemetryAsync($"Credential clientId in refreshed config '{refreshedClientId ?? "Empty"}' does not match the current credential clientId '{clientId}'.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure the credential authorizationUrl in the refreshed config match the current credential authorizationUrl for OAuth auth scheme
|
||||||
|
var authorizationUrl = _credData.Data.GetValueOrDefault("authorizationUrl", null);
|
||||||
|
var refreshedAuthorizationUrl = refreshedCredConfig.Data.GetValueOrDefault("authorizationUrl", null);
|
||||||
|
if (authorizationUrl != refreshedAuthorizationUrl)
|
||||||
|
{
|
||||||
|
Trace.Error($"Credential authorizationUrl in refreshed config '{refreshedAuthorizationUrl ?? "Empty"}' does not match the current credential authorizationUrl '{authorizationUrl}'.");
|
||||||
|
await ReportTelemetryAsync($"Credential authorizationUrl in refreshed config '{refreshedAuthorizationUrl ?? "Empty"}' does not match the current credential authorizationUrl '{authorizationUrl}'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save the refreshed runner credentials as a separate file
|
// save the refreshed runner credentials as a separate file
|
||||||
_store.SaveMigratedCredential(refreshedCredConfig);
|
_store.SaveMigratedCredential(refreshedCredConfig);
|
||||||
await ReportTelemetryAsync("Runner credentials updated successfully.");
|
|
||||||
|
if (refreshedCredConfig.Data.ContainsKey("authorizationUrlV2"))
|
||||||
|
{
|
||||||
|
HostContext.EnableAuthMigration("Credential file updated");
|
||||||
|
await ReportTelemetryAsync("Runner credentials updated successfully. Auth migration is enabled.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HostContext.DeferAuthMigration(TimeSpan.FromDays(365), "Credential file does not contain authorizationUrlV2");
|
||||||
|
await ReportTelemetryAsync("Runner credentials updated successfully. Auth migration is disabled.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> VerifyRunnerQualifiedId(string runnerQualifiedId)
|
private async Task<bool> VerifyRunnerQualifiedId(string runnerQualifiedId)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ namespace GitHub.Runner.Sdk
|
|||||||
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
|
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
|
||||||
{
|
{
|
||||||
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||||
|
RawClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawHeaderValues = new List<ProductInfoHeaderValue>();
|
var rawHeaderValues = new List<ProductInfoHeaderValue>();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<startup>
|
<startup>
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />
|
||||||
</startup>
|
</startup>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(PackageRuntime)' != 'win-arm64' ">
|
<PropertyGroup Condition=" '$(PackageRuntime)' != 'win-arm64' ">
|
||||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
|||||||
@@ -688,7 +688,8 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -450,7 +450,8 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
|
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(usingToken.Value, "node16", 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))
|
if (string.IsNullOrEmpty(mainToken?.Value))
|
||||||
{
|
{
|
||||||
@@ -490,7 +491,7 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
else
|
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)
|
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(
|
private void ConvertInputs(
|
||||||
|
|||||||
@@ -862,7 +862,21 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
ExpressionValues["secrets"] = Global.Variables.ToSecretsContext();
|
ExpressionValues["secrets"] = Global.Variables.ToSecretsContext();
|
||||||
ExpressionValues["runner"] = new RunnerContext();
|
ExpressionValues["runner"] = new RunnerContext();
|
||||||
ExpressionValues["job"] = new JobContext();
|
|
||||||
|
Trace.Info("Initializing Job context");
|
||||||
|
var jobContext = new JobContext();
|
||||||
|
if (Global.Variables.GetBoolean(Constants.Runner.Features.AddCheckRunIdToJobContext) ?? false)
|
||||||
|
{
|
||||||
|
ExpressionValues.TryGetValue("job", out var jobDictionary);
|
||||||
|
if (jobDictionary != null)
|
||||||
|
{
|
||||||
|
foreach (var pair in jobDictionary.AssertDictionary("job"))
|
||||||
|
{
|
||||||
|
jobContext[pair.Key] = pair.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExpressionValues["job"] = jobContext;
|
||||||
|
|
||||||
Trace.Info("Initialize GitHub context");
|
Trace.Info("Initialize GitHub context");
|
||||||
var githubAccessToken = new StringContextData(Global.Variables.Get("system.github.token"));
|
var githubAccessToken = new StringContextData(Global.Variables.Get("system.github.token"));
|
||||||
|
|||||||
@@ -58,10 +58,41 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
var nodeData = data as NodeJSActionExecutionData;
|
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 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) ||
|
if (string.Equals(nodeData.NodeVersion, "node12", StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
string.Equals(nodeData.NodeVersion, "node16", 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;
|
(handler as INodeScriptActionHandler).Data = nodeData;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -9,7 +8,6 @@ using GitHub.Runner.Common;
|
|||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
@@ -60,7 +58,14 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
|
|
||||||
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
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,
|
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)
|
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
||||||
{
|
{
|
||||||
// Optimistically use the default
|
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
|
||||||
string nodeExternal = preferredVersion;
|
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
|
||||||
|
if (!string.IsNullOrEmpty(warningMessage))
|
||||||
|
{
|
||||||
|
executionContext.Warning(warningMessage);
|
||||||
|
}
|
||||||
|
|
||||||
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
||||||
{
|
{
|
||||||
@@ -264,7 +273,14 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
|
|
||||||
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
|
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))
|
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
|
||||||
{
|
{
|
||||||
var os = Constants.Runner.Platform.ToString();
|
var os = Constants.Runner.Platform.ToString();
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace GitHub.Runner.Worker
|
|||||||
public sealed class IssueMatcher
|
public sealed class IssueMatcher
|
||||||
{
|
{
|
||||||
private string _defaultSeverity;
|
private string _defaultSeverity;
|
||||||
|
private string _defaultFromPath;
|
||||||
private string _owner;
|
private string _owner;
|
||||||
private IssuePattern[] _patterns;
|
private IssuePattern[] _patterns;
|
||||||
private IssueMatch[] _state;
|
private IssueMatch[] _state;
|
||||||
@@ -29,6 +30,7 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
_owner = config.Owner;
|
_owner = config.Owner;
|
||||||
_defaultSeverity = config.Severity;
|
_defaultSeverity = config.Severity;
|
||||||
|
_defaultFromPath = config.FromPath;
|
||||||
_patterns = config.Patterns.Select(x => new IssuePattern(x, timeout)).ToArray();
|
_patterns = config.Patterns.Select(x => new IssuePattern(x, timeout)).ToArray();
|
||||||
Reset();
|
Reset();
|
||||||
}
|
}
|
||||||
@@ -59,6 +61,19 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string DefaultFromPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_defaultFromPath == null)
|
||||||
|
{
|
||||||
|
_defaultFromPath = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _defaultFromPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IssueMatch Match(string line)
|
public IssueMatch Match(string line)
|
||||||
{
|
{
|
||||||
// Single pattern
|
// Single pattern
|
||||||
@@ -69,7 +84,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
if (regexMatch.Success)
|
if (regexMatch.Success)
|
||||||
{
|
{
|
||||||
return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity);
|
return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -110,7 +125,7 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return
|
// Return
|
||||||
return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity);
|
return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
|
||||||
}
|
}
|
||||||
// Not the last pattern
|
// Not the last pattern
|
||||||
else
|
else
|
||||||
@@ -184,7 +199,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
public sealed class IssueMatch
|
public sealed class IssueMatch
|
||||||
{
|
{
|
||||||
public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null)
|
public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null, string defaultFromPath = null)
|
||||||
{
|
{
|
||||||
File = runningMatch?.File ?? GetValue(groups, pattern.File);
|
File = runningMatch?.File ?? GetValue(groups, pattern.File);
|
||||||
Line = runningMatch?.Line ?? GetValue(groups, pattern.Line);
|
Line = runningMatch?.Line ?? GetValue(groups, pattern.Line);
|
||||||
@@ -198,6 +213,11 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
Severity = defaultSeverity;
|
Severity = defaultSeverity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(FromPath) && !string.IsNullOrEmpty(defaultFromPath))
|
||||||
|
{
|
||||||
|
FromPath = defaultFromPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string File { get; }
|
public string File { get; }
|
||||||
@@ -282,6 +302,9 @@ namespace GitHub.Runner.Worker
|
|||||||
[DataMember(Name = "pattern")]
|
[DataMember(Name = "pattern")]
|
||||||
private IssuePatternConfig[] _patterns;
|
private IssuePatternConfig[] _patterns;
|
||||||
|
|
||||||
|
[DataMember(Name = "fromPath")]
|
||||||
|
private string _fromPath;
|
||||||
|
|
||||||
public string Owner
|
public string Owner
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -318,6 +341,24 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string FromPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_fromPath == null)
|
||||||
|
{
|
||||||
|
_fromPath = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _fromPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fromPath = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IssuePatternConfig[] Patterns
|
public IssuePatternConfig[] Patterns
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
|
||||||
@@ -56,5 +56,31 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double? CheckRunId
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.TryGetValue("check_run_id", out var value) && value is NumberContextData number)
|
||||||
|
{
|
||||||
|
return number.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value.HasValue)
|
||||||
|
{
|
||||||
|
this["check_run_id"] = new NumberContextData(value.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this["check_run_id"] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,11 @@ namespace GitHub.Runner.Worker
|
|||||||
if (message.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out VariableValue orchestrationId) &&
|
if (message.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out VariableValue orchestrationId) &&
|
||||||
!string.IsNullOrEmpty(orchestrationId.Value))
|
!string.IsNullOrEmpty(orchestrationId.Value))
|
||||||
{
|
{
|
||||||
// make the orchestration id the first item in the user-agent header to avoid get truncated in server log.
|
if (!HostContext.UserAgents.Any(x => string.Equals(x.Product?.Name, "OrchestrationId", StringComparison.OrdinalIgnoreCase)))
|
||||||
HostContext.UserAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", 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));
|
||||||
|
}
|
||||||
|
|
||||||
// make sure orchestration id is in the user-agent header.
|
// make sure orchestration id is in the user-agent header.
|
||||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
|
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
|
||||||
@@ -318,24 +321,17 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (jobContext.Global.Variables.GetBoolean(Constants.Runner.Features.SkipRetryCompleteJobUponKnownErrors) ?? false)
|
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, default);
|
||||||
{
|
|
||||||
await runServer.CompleteJob2Async(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, default);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, default);
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (VssUnauthorizedException ex) when (jobContext.Global.Variables.GetBoolean(Constants.Runner.Features.SkipRetryCompleteJobUponKnownErrors) ?? false)
|
catch (VssUnauthorizedException ex)
|
||||||
{
|
{
|
||||||
Trace.Error($"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}.");
|
Trace.Error($"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}.");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
exceptions.Add(ex);
|
exceptions.Add(ex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (TaskOrchestrationJobNotFoundException ex) when (jobContext.Global.Variables.GetBoolean(Constants.Runner.Features.SkipRetryCompleteJobUponKnownErrors) ?? false)
|
catch (TaskOrchestrationJobNotFoundException ex)
|
||||||
{
|
{
|
||||||
Trace.Error($"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}.");
|
Trace.Error($"Catch exception while attempting to complete job {message.JobId}, job request {message.RequestId}.");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||||
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
||||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -106,6 +106,18 @@ namespace GitHub.Services.Common
|
|||||||
{
|
{
|
||||||
VssTraceActivity traceActivity = VssTraceActivity.Current;
|
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)
|
lock (m_thisLock)
|
||||||
{
|
{
|
||||||
// Ensure that we attempt to use the most appropriate authentication mechanism by default.
|
// 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 readonly HttpMessageHandler m_transportHandler;
|
||||||
private HttpMessageInvoker m_messageInvoker;
|
private HttpMessageInvoker m_messageInvoker;
|
||||||
private CredentialWrapper m_credentialWrapper;
|
private CredentialWrapper m_credentialWrapper;
|
||||||
|
|||||||
@@ -253,11 +253,12 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Truncate(string errorBody)
|
internal static string Truncate(string errorBody)
|
||||||
{
|
{
|
||||||
if (errorBody.Length > 100)
|
const int maxLength = 200;
|
||||||
|
if (errorBody.Length > maxLength)
|
||||||
{
|
{
|
||||||
return errorBody.Substring(0, 100) + "[truncated]";
|
return errorBody.Substring(0, maxLength) + "[truncated]";
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorBody;
|
return errorBody;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ namespace GitHub.Services.WebApi.Jwt
|
|||||||
HS256,
|
HS256,
|
||||||
|
|
||||||
[EnumMember]
|
[EnumMember]
|
||||||
RS256
|
RS256,
|
||||||
|
|
||||||
|
[EnumMember]
|
||||||
|
PS256,
|
||||||
}
|
}
|
||||||
|
|
||||||
//JsonWebToken is marked as DataContract so
|
//JsonWebToken is marked as DataContract so
|
||||||
@@ -286,6 +289,7 @@ namespace GitHub.Services.WebApi.Jwt
|
|||||||
{
|
{
|
||||||
case JWTAlgorithm.HS256:
|
case JWTAlgorithm.HS256:
|
||||||
case JWTAlgorithm.RS256:
|
case JWTAlgorithm.RS256:
|
||||||
|
case JWTAlgorithm.PS256:
|
||||||
return signingCredentials.SignData(bytes);
|
return signingCredentials.SignData(bytes);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace GitHub.Services.Launch.Contracts
|
|||||||
{
|
{
|
||||||
[DataMember(EmitDefaultValue = false, Name = "authentication")]
|
[DataMember(EmitDefaultValue = false, Name = "authentication")]
|
||||||
public ActionDownloadAuthenticationResponse Authentication { get; set; }
|
public ActionDownloadAuthenticationResponse Authentication { get; set; }
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false, Name = "package_details")]
|
[DataMember(EmitDefaultValue = false, Name = "package_details")]
|
||||||
public ActionDownloadPackageDetailsResponse PackageDetails { get; set; }
|
public ActionDownloadPackageDetailsResponse PackageDetails { get; set; }
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ namespace GitHub.Services.Launch.Contracts
|
|||||||
|
|
||||||
|
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public class ActionDownloadPackageDetailsResponse
|
public class ActionDownloadPackageDetailsResponse
|
||||||
{
|
{
|
||||||
[DataMember(EmitDefaultValue = false, Name = "version")]
|
[DataMember(EmitDefaultValue = false, Name = "version")]
|
||||||
public string Version { get; set; }
|
public string Version { get; set; }
|
||||||
@@ -81,4 +81,25 @@ namespace GitHub.Services.Launch.Contracts
|
|||||||
[DataMember(EmitDefaultValue = false, Name = "actions")]
|
[DataMember(EmitDefaultValue = false, Name = "actions")]
|
||||||
public IDictionary<string, ActionDownloadInfoResponse> Actions { get; set; }
|
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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Formatting;
|
using System.Net.Http.Formatting;
|
||||||
using System.Net.Http.Headers;
|
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)
|
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");
|
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
|
public async Task<ActionDownloadInfoCollection> GetResolveActionsDownloadInfoAsyncV2(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken)
|
||||||
private async Task<T> GetLaunchSignedURLResponse<R, T>(Uri uri, R request, 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))
|
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))
|
using (HttpContent content = new ObjectContent<R>(request, m_formatter))
|
||||||
{
|
{
|
||||||
requestMessage.Content = content;
|
requestMessage.Content = content;
|
||||||
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
|
return await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken);
|
||||||
{
|
|
||||||
return await ReadJsonContentAsync<T>(response, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -520,8 +520,8 @@ namespace GitHub.Services.Results.Client
|
|||||||
Number = r.Order.GetValueOrDefault(),
|
Number = r.Order.GetValueOrDefault(),
|
||||||
Name = r.Name,
|
Name = r.Name,
|
||||||
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
|
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
|
||||||
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat),
|
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture),
|
||||||
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat),
|
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture),
|
||||||
Conclusion = ConvertResultToConclusion(r.Result)
|
Conclusion = ConvertResultToConclusion(r.Result)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,21 @@ namespace GitHub.Services.WebApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override JWTAlgorithm SignatureAlgorithm
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (m_signaturePadding == RSASignaturePadding.Pss)
|
||||||
|
{
|
||||||
|
return JWTAlgorithm.PS256;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return base.SignatureAlgorithm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override Byte[] GetSignature(Byte[] input)
|
protected override Byte[] GetSignature(Byte[] input)
|
||||||
{
|
{
|
||||||
using (var rsa = m_factory())
|
using (var rsa = m_factory())
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using GitHub.Runner.Common.Util;
|
using System;
|
||||||
using System;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests
|
namespace GitHub.Runner.Common.Tests
|
||||||
@@ -172,6 +172,133 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void AuthMigrationDisabledByDefault()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", "100");
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.False(_hc.AllowAuthMigration);
|
||||||
|
|
||||||
|
// Change migration state is error free.
|
||||||
|
_hc.EnableAuthMigration("L0Test");
|
||||||
|
_hc.DeferAuthMigration(TimeSpan.FromHours(1), "L0Test");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", null);
|
||||||
|
// Cleanup.
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public async Task AuthMigrationReenableTaskNotRunningByDefault()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", "50");
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.False(_hc.AllowAuthMigration);
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(200));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", null);
|
||||||
|
// Cleanup.
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
var logFile = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"trace_{nameof(HostContextL0)}_{nameof(AuthMigrationReenableTaskNotRunningByDefault)}.log");
|
||||||
|
var logContent = await File.ReadAllTextAsync(logFile);
|
||||||
|
Assert.Contains("HostContext", logContent);
|
||||||
|
Assert.DoesNotContain("Auth migration defer timer", logContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public void AuthMigrationEnableDisable()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Arrange.
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
var eventFiredCount = 0;
|
||||||
|
_hc.AuthMigrationChanged += (sender, e) =>
|
||||||
|
{
|
||||||
|
eventFiredCount++;
|
||||||
|
Assert.Equal("L0Test", e.Trace);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
_hc.EnableAuthMigration("L0Test");
|
||||||
|
Assert.True(_hc.AllowAuthMigration);
|
||||||
|
|
||||||
|
_hc.DeferAuthMigration(TimeSpan.FromHours(1), "L0Test");
|
||||||
|
Assert.False(_hc.AllowAuthMigration);
|
||||||
|
Assert.Equal(2, eventFiredCount);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Cleanup.
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Common")]
|
||||||
|
public async Task AuthMigrationAutoReset()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", "100");
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
Setup();
|
||||||
|
|
||||||
|
var eventFiredCount = 0;
|
||||||
|
_hc.AuthMigrationChanged += (sender, e) =>
|
||||||
|
{
|
||||||
|
eventFiredCount++;
|
||||||
|
Assert.NotEmpty(e.Trace);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
_hc.EnableAuthMigration("L0Test");
|
||||||
|
Assert.True(_hc.AllowAuthMigration);
|
||||||
|
|
||||||
|
_hc.DeferAuthMigration(TimeSpan.FromMilliseconds(500), "L0Test");
|
||||||
|
Assert.False(_hc.AllowAuthMigration);
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||||
|
Assert.True(_hc.AllowAuthMigration);
|
||||||
|
Assert.Equal(3, eventFiredCount);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("_GITHUB_ACTION_AUTH_MIGRATION_REFRESH_INTERVAL", null);
|
||||||
|
|
||||||
|
// Cleanup.
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Setup([CallerMemberName] string testName = "")
|
private void Setup([CallerMemberName] string testName = "")
|
||||||
{
|
{
|
||||||
_tokenSource = new CancellationTokenSource();
|
_tokenSource = new CancellationTokenSource();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -18,8 +19,6 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
private readonly Mock<IBrokerServer> _brokerServer;
|
private readonly Mock<IBrokerServer> _brokerServer;
|
||||||
private readonly Mock<IRunnerServer> _runnerServer;
|
private readonly Mock<IRunnerServer> _runnerServer;
|
||||||
private readonly Mock<ICredentialManager> _credMgr;
|
private readonly Mock<ICredentialManager> _credMgr;
|
||||||
private Mock<IConfigurationStore> _store;
|
|
||||||
|
|
||||||
|
|
||||||
public BrokerMessageListenerL0()
|
public BrokerMessageListenerL0()
|
||||||
{
|
{
|
||||||
@@ -27,7 +26,6 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
_config = new Mock<IConfigurationManager>();
|
_config = new Mock<IConfigurationManager>();
|
||||||
_config.Setup(x => x.LoadSettings()).Returns(_settings);
|
_config.Setup(x => x.LoadSettings()).Returns(_settings);
|
||||||
_credMgr = new Mock<ICredentialManager>();
|
_credMgr = new Mock<ICredentialManager>();
|
||||||
_store = new Mock<IConfigurationStore>();
|
|
||||||
_brokerServer = new Mock<IBrokerServer>();
|
_brokerServer = new Mock<IBrokerServer>();
|
||||||
_runnerServer = new Mock<IRunnerServer>();
|
_runnerServer = new Mock<IRunnerServer>();
|
||||||
}
|
}
|
||||||
@@ -35,7 +33,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void CreatesSession()
|
public async Task CreatesSession()
|
||||||
{
|
{
|
||||||
using (TestHostContext tc = CreateTestContext())
|
using (TestHostContext tc = CreateTestContext())
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
@@ -50,9 +48,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
tokenSource.Token))
|
tokenSource.Token))
|
||||||
.Returns(Task.FromResult(expectedSession));
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
|
||||||
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
|
||||||
|
|
||||||
// Act.
|
// Act.
|
||||||
BrokerMessageListener listener = new();
|
BrokerMessageListener listener = new();
|
||||||
@@ -70,12 +66,351 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task HandleAuthMigrationChanged()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
BrokerMessageListener listener = new();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
trace.Info("result: {0}", result);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token), Times.Once());
|
||||||
|
|
||||||
|
tc.EnableAuthMigration("L0Test");
|
||||||
|
|
||||||
|
var traceFile = Path.GetTempFileName();
|
||||||
|
File.Copy(tc.TraceFileName, traceFile, true);
|
||||||
|
Assert.Contains("Auth migration changed", File.ReadAllText(traceFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task CreatesSession_DeferAuthMigration()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
var throwException = true;
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(async (TaskAgentSession session, CancellationToken token) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (throwException)
|
||||||
|
{
|
||||||
|
throwException = false;
|
||||||
|
throw new NotSupportedException("Error during create session");
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectedSession;
|
||||||
|
});
|
||||||
|
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
BrokerMessageListener listener = new();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
tc.EnableAuthMigration("L0Test");
|
||||||
|
Assert.True(tc.AllowAuthMigration);
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
trace.Info("result: {0}", result);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token), Times.Exactly(2));
|
||||||
|
_credMgr.Verify(x => x.LoadCredentials(true), Times.Exactly(2));
|
||||||
|
|
||||||
|
Assert.False(tc.AllowAuthMigration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task GetNextMessage()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
var expectedMessage = new TaskAgentMessage();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.GetRunnerMessageAsync(
|
||||||
|
It.IsAny<Guid?>(),
|
||||||
|
It.IsAny<TaskAgentStatus>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<bool>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(expectedMessage));
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
BrokerMessageListener listener = new();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
trace.Info("result: {0}", result);
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
|
||||||
|
TaskAgentMessage message = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
trace.Info("message: {0}", message);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(expectedMessage, message);
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.GetRunnerMessageAsync(
|
||||||
|
It.IsAny<Guid?>(),
|
||||||
|
It.IsAny<TaskAgentStatus>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<bool>(),
|
||||||
|
It.IsAny<CancellationToken>()), Times.Once());
|
||||||
|
|
||||||
|
_brokerServer.Verify(x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<VssCredentials>()), Times.Once());
|
||||||
|
|
||||||
|
_credMgr.Verify(x => x.LoadCredentials(true), Times.Once());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task GetNextMessage_EnableAuthMigration()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
var expectedMessage = new TaskAgentMessage();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.GetRunnerMessageAsync(
|
||||||
|
It.IsAny<Guid?>(),
|
||||||
|
It.IsAny<TaskAgentStatus>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<bool>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(expectedMessage));
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
BrokerMessageListener listener = new();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
trace.Info("result: {0}", result);
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
|
||||||
|
tc.EnableAuthMigration("L0Test");
|
||||||
|
|
||||||
|
TaskAgentMessage message = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
trace.Info("message: {0}", message);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(expectedMessage, message);
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.GetRunnerMessageAsync(
|
||||||
|
It.IsAny<Guid?>(),
|
||||||
|
It.IsAny<TaskAgentStatus>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<bool>(),
|
||||||
|
It.IsAny<CancellationToken>()), Times.Once());
|
||||||
|
|
||||||
|
_brokerServer.Verify(x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<VssCredentials>()), Times.Exactly(2));
|
||||||
|
|
||||||
|
_credMgr.Verify(x => x.LoadCredentials(true), Times.Exactly(2));
|
||||||
|
|
||||||
|
Assert.True(tc.AllowAuthMigration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task GetNextMessage_AuthMigrationFallback()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
tc.EnableAuthMigration("L0Test");
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
var expectedMessage = new TaskAgentMessage();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.GetRunnerMessageAsync(
|
||||||
|
It.IsAny<Guid?>(),
|
||||||
|
It.IsAny<TaskAgentStatus>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<bool>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate, CancellationToken token) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (tc.AllowAuthMigration)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Error during get message");
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectedMessage;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
BrokerMessageListener listener = new();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
trace.Info("result: {0}", result);
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
|
||||||
|
Assert.True(tc.AllowAuthMigration);
|
||||||
|
|
||||||
|
TaskAgentMessage message = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
trace.Info("message: {0}", message);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(expectedMessage, message);
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.GetRunnerMessageAsync(
|
||||||
|
It.IsAny<Guid?>(),
|
||||||
|
It.IsAny<TaskAgentStatus>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<bool>(),
|
||||||
|
It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
|
|
||||||
|
_brokerServer.Verify(x => x.ConnectAsync(It.IsAny<Uri>(), It.IsAny<VssCredentials>()), Times.Exactly(3));
|
||||||
|
|
||||||
|
_credMgr.Verify(x => x.LoadCredentials(true), Times.Exactly(3));
|
||||||
|
|
||||||
|
Assert.False(tc.AllowAuthMigration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task CreatesSessionWithProvidedSettings()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
// Make sure the config is never called when settings are provided
|
||||||
|
_config.Setup(x => x.LoadSettings()).Throws(new InvalidOperationException("Should not be called"));
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
// Use the constructor that accepts settings
|
||||||
|
BrokerMessageListener listener = new(_settings);
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
trace.Info("result: {0}", result);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token), Times.Once());
|
||||||
|
|
||||||
|
// Verify LoadSettings was never called
|
||||||
|
_config.Verify(x => x.LoadSettings(), Times.Never());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
|
private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
|
||||||
{
|
{
|
||||||
TestHostContext tc = new(this, testName);
|
TestHostContext tc = new(this, testName);
|
||||||
tc.SetSingleton<IConfigurationManager>(_config.Object);
|
tc.SetSingleton<IConfigurationManager>(_config.Object);
|
||||||
tc.SetSingleton<ICredentialManager>(_credMgr.Object);
|
tc.SetSingleton<ICredentialManager>(_credMgr.Object);
|
||||||
tc.SetSingleton<IConfigurationStore>(_store.Object);
|
|
||||||
tc.SetSingleton<IBrokerServer>(_brokerServer.Object);
|
tc.SetSingleton<IBrokerServer>(_brokerServer.Object);
|
||||||
tc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
tc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
return tc;
|
return tc;
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
using GitHub.Runner.Listener;
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using GitHub.Runner.Listener;
|
||||||
using GitHub.Runner.Listener.Configuration;
|
using GitHub.Runner.Listener.Configuration;
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.OAuth;
|
using GitHub.Services.OAuth;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
||||||
{
|
{
|
||||||
public class TestRunnerCredential : CredentialProvider
|
public class TestRunnerCredential : CredentialProvider
|
||||||
{
|
{
|
||||||
public TestRunnerCredential() : base("TEST") { }
|
public TestRunnerCredential() : base("TEST") { }
|
||||||
public override VssCredentials GetVssCredentials(IHostContext context)
|
public override VssCredentials GetVssCredentials(IHostContext context, bool allowAuthUrlV2)
|
||||||
{
|
{
|
||||||
Tracing trace = context.GetTrace("OuthAccessToken");
|
Tracing trace = context.GetTrace("OuthAccessToken");
|
||||||
trace.Info("GetVssCredentials()");
|
trace.Info("GetVssCredentials()");
|
||||||
@@ -23,4 +27,85 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public class OAuthCredentialTestsL0
|
||||||
|
{
|
||||||
|
private Mock<IRSAKeyManager> _rsaKeyManager = new Mock<IRSAKeyManager>();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "OAuthCredential")]
|
||||||
|
public void NotUseAuthV2Url()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = new(this))
|
||||||
|
{
|
||||||
|
// Arrange.
|
||||||
|
var oauth = new OAuthCredential();
|
||||||
|
oauth.CredentialData = new CredentialData()
|
||||||
|
{
|
||||||
|
Scheme = Constants.Configuration.OAuth
|
||||||
|
};
|
||||||
|
oauth.CredentialData.Data.Add("clientId", "someClientId");
|
||||||
|
oauth.CredentialData.Data.Add("authorizationUrl", "http://myserver/");
|
||||||
|
oauth.CredentialData.Data.Add("authorizationUrlV2", "http://myserverv2/");
|
||||||
|
|
||||||
|
_rsaKeyManager.Setup(x => x.GetKey()).Returns(RSA.Create(2048));
|
||||||
|
hc.SetSingleton<IRSAKeyManager>(_rsaKeyManager.Object);
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
var cred = oauth.GetVssCredentials(hc, false); // not allow auth v2
|
||||||
|
|
||||||
|
var cred2 = oauth.GetVssCredentials(hc, true); // use auth v2 but hostcontext doesn't
|
||||||
|
|
||||||
|
hc.EnableAuthMigration("L0Test");
|
||||||
|
var cred3 = oauth.GetVssCredentials(hc, false); // not use auth v2 but hostcontext does
|
||||||
|
|
||||||
|
oauth.CredentialData.Data.Remove("authorizationUrlV2");
|
||||||
|
var cred4 = oauth.GetVssCredentials(hc, true); // v2 url is not there
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal("http://myserver/", (cred.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
|
||||||
|
Assert.Equal("someClientId", (cred.Federated as VssOAuthCredential).ClientCredential.ClientId);
|
||||||
|
|
||||||
|
Assert.Equal("http://myserver/", (cred2.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
|
||||||
|
Assert.Equal("someClientId", (cred2.Federated as VssOAuthCredential).ClientCredential.ClientId);
|
||||||
|
|
||||||
|
Assert.Equal("http://myserver/", (cred3.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
|
||||||
|
Assert.Equal("someClientId", (cred3.Federated as VssOAuthCredential).ClientCredential.ClientId);
|
||||||
|
|
||||||
|
Assert.Equal("http://myserver/", (cred4.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
|
||||||
|
Assert.Equal("someClientId", (cred4.Federated as VssOAuthCredential).ClientCredential.ClientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "OAuthCredential")]
|
||||||
|
public void UseAuthV2Url()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = new(this))
|
||||||
|
{
|
||||||
|
// Arrange.
|
||||||
|
var oauth = new OAuthCredential();
|
||||||
|
oauth.CredentialData = new CredentialData()
|
||||||
|
{
|
||||||
|
Scheme = Constants.Configuration.OAuth
|
||||||
|
};
|
||||||
|
oauth.CredentialData.Data.Add("clientId", "someClientId");
|
||||||
|
oauth.CredentialData.Data.Add("authorizationUrl", "http://myserver/");
|
||||||
|
oauth.CredentialData.Data.Add("authorizationUrlV2", "http://myserverv2/");
|
||||||
|
|
||||||
|
_rsaKeyManager.Setup(x => x.GetKey()).Returns(RSA.Create(2048));
|
||||||
|
hc.SetSingleton<IRSAKeyManager>(_rsaKeyManager.Object);
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
hc.EnableAuthMigration("L0Test");
|
||||||
|
var cred = oauth.GetVssCredentials(hc, true);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal("http://myserverv2/", (cred.Federated as VssOAuthCredential).AuthorizationUrl.AbsoluteUri);
|
||||||
|
Assert.Equal("someClientId", (cred.Federated as VssOAuthCredential).ClientCredential.ClientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void CreatesSession()
|
public async Task CreatesSession()
|
||||||
{
|
{
|
||||||
using (TestHostContext tc = CreateTestContext())
|
using (TestHostContext tc = CreateTestContext())
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
@@ -67,7 +67,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
tokenSource.Token))
|
tokenSource.Token))
|
||||||
.Returns(Task.FromResult(expectedSession));
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
||||||
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
||||||
|
|
||||||
@@ -95,69 +95,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void CreatesSessionWithBrokerMigration()
|
public async Task DeleteSession()
|
||||||
{
|
|
||||||
using (TestHostContext tc = CreateTestContext())
|
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
|
||||||
{
|
|
||||||
Tracing trace = tc.GetTrace();
|
|
||||||
|
|
||||||
// Arrange.
|
|
||||||
var expectedSession = new TaskAgentSession()
|
|
||||||
{
|
|
||||||
OwnerName = "legacy",
|
|
||||||
BrokerMigrationMessage = new BrokerMigrationMessage(new Uri("https://broker.actions.github.com"))
|
|
||||||
};
|
|
||||||
|
|
||||||
var expectedBrokerSession = new TaskAgentSession()
|
|
||||||
{
|
|
||||||
OwnerName = "broker"
|
|
||||||
};
|
|
||||||
|
|
||||||
_runnerServer
|
|
||||||
.Setup(x => x.CreateAgentSessionAsync(
|
|
||||||
_settings.PoolId,
|
|
||||||
It.Is<TaskAgentSession>(y => y != null),
|
|
||||||
tokenSource.Token))
|
|
||||||
.Returns(Task.FromResult(expectedSession));
|
|
||||||
|
|
||||||
_brokerServer
|
|
||||||
.Setup(x => x.CreateSessionAsync(
|
|
||||||
It.Is<TaskAgentSession>(y => y != null),
|
|
||||||
tokenSource.Token))
|
|
||||||
.Returns(Task.FromResult(expectedBrokerSession));
|
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
|
||||||
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
|
||||||
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
|
||||||
|
|
||||||
// Act.
|
|
||||||
MessageListener listener = new();
|
|
||||||
listener.Initialize(tc);
|
|
||||||
|
|
||||||
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
|
||||||
trace.Info("result: {0}", result);
|
|
||||||
|
|
||||||
// Assert.
|
|
||||||
Assert.Equal(CreateSessionResult.Success, result);
|
|
||||||
|
|
||||||
_runnerServer
|
|
||||||
.Verify(x => x.CreateAgentSessionAsync(
|
|
||||||
_settings.PoolId,
|
|
||||||
It.Is<TaskAgentSession>(y => y != null),
|
|
||||||
tokenSource.Token), Times.Once());
|
|
||||||
|
|
||||||
_brokerServer
|
|
||||||
.Verify(x => x.CreateSessionAsync(
|
|
||||||
It.Is<TaskAgentSession>(y => y != null),
|
|
||||||
tokenSource.Token), Times.Once());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
[Trait("Level", "L0")]
|
|
||||||
[Trait("Category", "Runner")]
|
|
||||||
public async void DeleteSession()
|
|
||||||
{
|
{
|
||||||
using (TestHostContext tc = CreateTestContext())
|
using (TestHostContext tc = CreateTestContext())
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
@@ -177,7 +115,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
tokenSource.Token))
|
tokenSource.Token))
|
||||||
.Returns(Task.FromResult(expectedSession));
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
||||||
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
||||||
|
|
||||||
@@ -204,84 +142,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void DeleteSessionWithBrokerMigration()
|
public async Task GetNextMessage()
|
||||||
{
|
|
||||||
using (TestHostContext tc = CreateTestContext())
|
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
|
||||||
{
|
|
||||||
Tracing trace = tc.GetTrace();
|
|
||||||
|
|
||||||
// Arrange.
|
|
||||||
var expectedSession = new TaskAgentSession()
|
|
||||||
{
|
|
||||||
OwnerName = "legacy",
|
|
||||||
BrokerMigrationMessage = new BrokerMigrationMessage(new Uri("https://broker.actions.github.com"))
|
|
||||||
};
|
|
||||||
|
|
||||||
var expectedBrokerSession = new TaskAgentSession()
|
|
||||||
{
|
|
||||||
SessionId = Guid.NewGuid(),
|
|
||||||
OwnerName = "broker"
|
|
||||||
};
|
|
||||||
|
|
||||||
_runnerServer
|
|
||||||
.Setup(x => x.CreateAgentSessionAsync(
|
|
||||||
_settings.PoolId,
|
|
||||||
It.Is<TaskAgentSession>(y => y != null),
|
|
||||||
tokenSource.Token))
|
|
||||||
.Returns(Task.FromResult(expectedSession));
|
|
||||||
|
|
||||||
_brokerServer
|
|
||||||
.Setup(x => x.CreateSessionAsync(
|
|
||||||
It.Is<TaskAgentSession>(y => y != null),
|
|
||||||
tokenSource.Token))
|
|
||||||
.Returns(Task.FromResult(expectedBrokerSession));
|
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
|
||||||
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
|
||||||
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
|
||||||
|
|
||||||
// Act.
|
|
||||||
MessageListener listener = new();
|
|
||||||
listener.Initialize(tc);
|
|
||||||
|
|
||||||
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
|
||||||
trace.Info("result: {0}", result);
|
|
||||||
|
|
||||||
Assert.Equal(CreateSessionResult.Success, result);
|
|
||||||
|
|
||||||
_runnerServer
|
|
||||||
.Verify(x => x.CreateAgentSessionAsync(
|
|
||||||
_settings.PoolId,
|
|
||||||
It.Is<TaskAgentSession>(y => y != null),
|
|
||||||
tokenSource.Token), Times.Once());
|
|
||||||
|
|
||||||
_brokerServer
|
|
||||||
.Verify(x => x.CreateSessionAsync(
|
|
||||||
It.Is<TaskAgentSession>(y => y != null),
|
|
||||||
tokenSource.Token), Times.Once());
|
|
||||||
|
|
||||||
_brokerServer
|
|
||||||
.Setup(x => x.DeleteSessionAsync(It.IsAny<CancellationToken>()))
|
|
||||||
.Returns(Task.CompletedTask);
|
|
||||||
|
|
||||||
// Act.
|
|
||||||
await listener.DeleteSessionAsync();
|
|
||||||
|
|
||||||
|
|
||||||
//Assert
|
|
||||||
_runnerServer
|
|
||||||
.Verify(x => x.DeleteAgentSessionAsync(
|
|
||||||
_settings.PoolId, expectedBrokerSession.SessionId, It.IsAny<CancellationToken>()), Times.Once());
|
|
||||||
_brokerServer
|
|
||||||
.Verify(x => x.DeleteSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
[Trait("Level", "L0")]
|
|
||||||
[Trait("Category", "Runner")]
|
|
||||||
public async void GetNextMessage()
|
|
||||||
{
|
{
|
||||||
using (TestHostContext tc = CreateTestContext())
|
using (TestHostContext tc = CreateTestContext())
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
@@ -301,7 +162,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
tokenSource.Token))
|
tokenSource.Token))
|
||||||
.Returns(Task.FromResult(expectedSession));
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
||||||
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
||||||
|
|
||||||
@@ -362,7 +223,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void GetNextMessageWithBrokerMigration()
|
public async Task GetNextMessageWithBrokerMigration()
|
||||||
{
|
{
|
||||||
using (TestHostContext tc = CreateTestContext())
|
using (TestHostContext tc = CreateTestContext())
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
@@ -382,7 +243,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
tokenSource.Token))
|
tokenSource.Token))
|
||||||
.Returns(Task.FromResult(expectedSession));
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
||||||
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
||||||
|
|
||||||
@@ -462,13 +323,22 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
_brokerServer
|
_brokerServer
|
||||||
.Verify(x => x.GetRunnerMessageAsync(
|
.Verify(x => x.GetRunnerMessageAsync(
|
||||||
expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length));
|
expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length));
|
||||||
|
|
||||||
|
_credMgr
|
||||||
|
.Verify(x => x.LoadCredentials(true), Times.Exactly(brokerMessages.Length));
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.UpdateConnectionIfNeeded(brokerMigrationMesage.BrokerBaseUrl, It.IsAny<VssCredentials>()), Times.Exactly(brokerMessages.Length));
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.ForceRefreshConnection(It.IsAny<VssCredentials>()), Times.Never);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void CreateSessionWithOriginalCredential()
|
public async Task CreateSessionWithOriginalCredential()
|
||||||
{
|
{
|
||||||
using (TestHostContext tc = CreateTestContext())
|
using (TestHostContext tc = CreateTestContext())
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
@@ -484,7 +354,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
tokenSource.Token))
|
tokenSource.Token))
|
||||||
.Returns(Task.FromResult(expectedSession));
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
|
|
||||||
var originalCred = new CredentialData() { Scheme = Constants.Configuration.OAuth };
|
var originalCred = new CredentialData() { Scheme = Constants.Configuration.OAuth };
|
||||||
originalCred.Data["authorizationUrl"] = "https://s.server";
|
originalCred.Data["authorizationUrl"] = "https://s.server";
|
||||||
@@ -513,7 +383,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void SkipDeleteSession_WhenGetNextMessageGetTaskAgentAccessTokenExpiredException()
|
public async Task SkipDeleteSession_WhenGetNextMessageGetTaskAgentAccessTokenExpiredException()
|
||||||
{
|
{
|
||||||
using (TestHostContext tc = CreateTestContext())
|
using (TestHostContext tc = CreateTestContext())
|
||||||
using (var tokenSource = new CancellationTokenSource())
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
@@ -533,7 +403,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
tokenSource.Token))
|
tokenSource.Token))
|
||||||
.Returns(Task.FromResult(expectedSession));
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
_credMgr.Setup(x => x.LoadCredentials()).Returns(new VssCredentials());
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
||||||
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
||||||
|
|
||||||
@@ -571,5 +441,301 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
_settings.PoolId, expectedSession.SessionId, It.IsAny<CancellationToken>()), Times.Never);
|
_settings.PoolId, expectedSession.SessionId, It.IsAny<CancellationToken>()), Times.Never);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task HandleAuthMigrationChanged()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
_runnerServer
|
||||||
|
.Setup(x => x.CreateAgentSessionAsync(
|
||||||
|
_settings.PoolId,
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
MessageListener listener = new();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
trace.Info("result: {0}", result);
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
_runnerServer
|
||||||
|
.Verify(x => x.CreateAgentSessionAsync(
|
||||||
|
_settings.PoolId,
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token), Times.Once());
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.CreateSessionAsync(
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token), Times.Never());
|
||||||
|
|
||||||
|
tc.EnableAuthMigration("L0Test");
|
||||||
|
|
||||||
|
var traceFile = Path.GetTempFileName();
|
||||||
|
File.Copy(tc.TraceFileName, traceFile, true);
|
||||||
|
Assert.Contains("Auth migration changed", File.ReadAllText(traceFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task GetNextMessageWithBrokerMigration_AuthMigrationFallback()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
PropertyInfo sessionIdProperty = expectedSession.GetType().GetProperty("SessionId", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
Assert.NotNull(sessionIdProperty);
|
||||||
|
sessionIdProperty.SetValue(expectedSession, Guid.NewGuid());
|
||||||
|
|
||||||
|
_runnerServer
|
||||||
|
.Setup(x => x.CreateAgentSessionAsync(
|
||||||
|
_settings.PoolId,
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
|
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
||||||
|
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
MessageListener listener = new();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
tc.EnableAuthMigration("L0Test");
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
|
||||||
|
var brokerMigrationMesage = new BrokerMigrationMessage(new Uri("https://actions.broker.com"));
|
||||||
|
|
||||||
|
var arMessages = new TaskAgentMessage[]
|
||||||
|
{
|
||||||
|
new TaskAgentMessage
|
||||||
|
{
|
||||||
|
Body = JsonUtility.ToString(brokerMigrationMesage),
|
||||||
|
MessageType = BrokerMigrationMessage.MessageType
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var brokerMessages = new TaskAgentMessage[]
|
||||||
|
{
|
||||||
|
new TaskAgentMessage
|
||||||
|
{
|
||||||
|
Body = "somebody1",
|
||||||
|
MessageId = 4234,
|
||||||
|
MessageType = JobRequestMessageTypes.PipelineAgentJobRequest
|
||||||
|
},
|
||||||
|
new TaskAgentMessage
|
||||||
|
{
|
||||||
|
Body = "somebody2",
|
||||||
|
MessageId = 4235,
|
||||||
|
MessageType = JobCancelMessage.MessageType
|
||||||
|
},
|
||||||
|
null, //should be skipped by GetNextMessageAsync implementation
|
||||||
|
null,
|
||||||
|
new TaskAgentMessage
|
||||||
|
{
|
||||||
|
Body = "somebody3",
|
||||||
|
MessageId = 4236,
|
||||||
|
MessageType = JobRequestMessageTypes.PipelineAgentJobRequest
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var brokerMessageQueue = new Queue<TaskAgentMessage>(brokerMessages);
|
||||||
|
|
||||||
|
_runnerServer
|
||||||
|
.Setup(x => x.GetAgentMessageAsync(
|
||||||
|
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, string runnerVersion, string os, string architecture, bool disableUpdate, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return arMessages[0]; // always send migration message
|
||||||
|
});
|
||||||
|
|
||||||
|
var counter = 0;
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.GetRunnerMessageAsync(
|
||||||
|
expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (Guid sessionId, TaskAgentStatus status, string runnerVersion, string os, string architecture, bool disableUpdate, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
counter++;
|
||||||
|
await Task.Yield();
|
||||||
|
if (counter == 2)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Something wrong.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return brokerMessageQueue.Dequeue();
|
||||||
|
});
|
||||||
|
|
||||||
|
TaskAgentMessage message1 = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
TaskAgentMessage message2 = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
TaskAgentMessage message3 = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
Assert.Equal(brokerMessages[0], message1);
|
||||||
|
Assert.Equal(brokerMessages[1], message2);
|
||||||
|
Assert.Equal(brokerMessages[4], message3);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
_runnerServer
|
||||||
|
.Verify(x => x.GetAgentMessageAsync(
|
||||||
|
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length + 1));
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.GetRunnerMessageAsync(
|
||||||
|
expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length + 1));
|
||||||
|
|
||||||
|
_credMgr
|
||||||
|
.Verify(x => x.LoadCredentials(true), Times.Exactly(brokerMessages.Length + 1));
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.UpdateConnectionIfNeeded(brokerMigrationMesage.BrokerBaseUrl, It.IsAny<VssCredentials>()), Times.Exactly(brokerMessages.Length + 1));
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.ForceRefreshConnection(It.IsAny<VssCredentials>()), Times.Once());
|
||||||
|
|
||||||
|
Assert.False(tc.AllowAuthMigration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task GetNextMessageWithBrokerMigration_EnableAuthMigration()
|
||||||
|
{
|
||||||
|
using (TestHostContext tc = CreateTestContext())
|
||||||
|
using (var tokenSource = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
Tracing trace = tc.GetTrace();
|
||||||
|
|
||||||
|
// Arrange.
|
||||||
|
var expectedSession = new TaskAgentSession();
|
||||||
|
PropertyInfo sessionIdProperty = expectedSession.GetType().GetProperty("SessionId", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
Assert.NotNull(sessionIdProperty);
|
||||||
|
sessionIdProperty.SetValue(expectedSession, Guid.NewGuid());
|
||||||
|
|
||||||
|
_runnerServer
|
||||||
|
.Setup(x => x.CreateAgentSessionAsync(
|
||||||
|
_settings.PoolId,
|
||||||
|
It.Is<TaskAgentSession>(y => y != null),
|
||||||
|
tokenSource.Token))
|
||||||
|
.Returns(Task.FromResult(expectedSession));
|
||||||
|
|
||||||
|
_credMgr.Setup(x => x.LoadCredentials(It.IsAny<bool>())).Returns(new VssCredentials());
|
||||||
|
_store.Setup(x => x.GetCredentials()).Returns(new CredentialData() { Scheme = Constants.Configuration.OAuthAccessToken });
|
||||||
|
_store.Setup(x => x.GetMigratedCredentials()).Returns(default(CredentialData));
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
MessageListener listener = new();
|
||||||
|
listener.Initialize(tc);
|
||||||
|
|
||||||
|
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||||
|
Assert.Equal(CreateSessionResult.Success, result);
|
||||||
|
|
||||||
|
var brokerMigrationMesage = new BrokerMigrationMessage(new Uri("https://actions.broker.com"));
|
||||||
|
|
||||||
|
var arMessages = new TaskAgentMessage[]
|
||||||
|
{
|
||||||
|
new TaskAgentMessage
|
||||||
|
{
|
||||||
|
Body = JsonUtility.ToString(brokerMigrationMesage),
|
||||||
|
MessageType = BrokerMigrationMessage.MessageType
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var brokerMessages = new TaskAgentMessage[]
|
||||||
|
{
|
||||||
|
new TaskAgentMessage
|
||||||
|
{
|
||||||
|
Body = "somebody1",
|
||||||
|
MessageId = 4234,
|
||||||
|
MessageType = JobRequestMessageTypes.PipelineAgentJobRequest
|
||||||
|
},
|
||||||
|
new TaskAgentMessage
|
||||||
|
{
|
||||||
|
Body = "somebody2",
|
||||||
|
MessageId = 4235,
|
||||||
|
MessageType = JobCancelMessage.MessageType
|
||||||
|
},
|
||||||
|
null, //should be skipped by GetNextMessageAsync implementation
|
||||||
|
null,
|
||||||
|
new TaskAgentMessage
|
||||||
|
{
|
||||||
|
Body = "somebody3",
|
||||||
|
MessageId = 4236,
|
||||||
|
MessageType = JobRequestMessageTypes.PipelineAgentJobRequest
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var brokerMessageQueue = new Queue<TaskAgentMessage>(brokerMessages);
|
||||||
|
|
||||||
|
_runnerServer
|
||||||
|
.Setup(x => x.GetAgentMessageAsync(
|
||||||
|
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, string runnerVersion, string os, string architecture, bool disableUpdate, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return arMessages[0]; // always send migration message
|
||||||
|
});
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Setup(x => x.GetRunnerMessageAsync(
|
||||||
|
expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (Guid sessionId, TaskAgentStatus status, string runnerVersion, string os, string architecture, bool disableUpdate, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (!tc.AllowAuthMigration)
|
||||||
|
{
|
||||||
|
tc.EnableAuthMigration("L0Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
return brokerMessageQueue.Dequeue();
|
||||||
|
});
|
||||||
|
|
||||||
|
TaskAgentMessage message1 = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
TaskAgentMessage message2 = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
TaskAgentMessage message3 = await listener.GetNextMessageAsync(tokenSource.Token);
|
||||||
|
Assert.Equal(brokerMessages[0], message1);
|
||||||
|
Assert.Equal(brokerMessages[1], message2);
|
||||||
|
Assert.Equal(brokerMessages[4], message3);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
_runnerServer
|
||||||
|
.Verify(x => x.GetAgentMessageAsync(
|
||||||
|
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length));
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.GetRunnerMessageAsync(
|
||||||
|
expectedSession.SessionId, TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(brokerMessages.Length));
|
||||||
|
|
||||||
|
_credMgr
|
||||||
|
.Verify(x => x.LoadCredentials(true), Times.Exactly(brokerMessages.Length));
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.UpdateConnectionIfNeeded(brokerMigrationMesage.BrokerBaseUrl, It.IsAny<VssCredentials>()), Times.Exactly(brokerMessages.Length));
|
||||||
|
|
||||||
|
_brokerServer
|
||||||
|
.Verify(x => x.ForceRefreshConnection(It.IsAny<VssCredentials>()), Times.Once());
|
||||||
|
|
||||||
|
Assert.True(tc.AllowAuthMigration);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.Runner.Listener;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
using GitHub.Runner.Common.Tests;
|
||||||
|
using GitHub.Runner.Listener;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using System.Threading;
|
|
||||||
using GitHub.Runner.Common.Tests;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Tests.Listener
|
namespace GitHub.Runner.Tests.Listener
|
||||||
{
|
{
|
||||||
@@ -210,9 +210,9 @@ namespace GitHub.Runner.Tests.Listener
|
|||||||
var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(credData)));
|
var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(credData)));
|
||||||
_runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny<int>(), It.Is<string>(s => s == "credentials"), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(encodedConfig);
|
_runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny<int>(), It.Is<string>(s => s == "credentials"), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(encodedConfig);
|
||||||
|
|
||||||
|
|
||||||
var _runnerConfigUpdater = new RunnerConfigUpdater();
|
var _runnerConfigUpdater = new RunnerConfigUpdater();
|
||||||
_runnerConfigUpdater.Initialize(hc);
|
_runnerConfigUpdater.Initialize(hc);
|
||||||
|
hc.EnableAuthMigration("L0Test");
|
||||||
|
|
||||||
var validRunnerQualifiedId = "valid/runner/qualifiedid/1";
|
var validRunnerQualifiedId = "valid/runner/qualifiedid/1";
|
||||||
var configType = "credentials";
|
var configType = "credentials";
|
||||||
@@ -226,6 +226,7 @@ namespace GitHub.Runner.Tests.Listener
|
|||||||
_runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "credentials", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
_runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "credentials", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("Runner credentials updated successfully")), It.IsAny<CancellationToken>()), Times.Once);
|
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("Runner credentials updated successfully")), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
_configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny<CredentialData>()), Times.Once);
|
_configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny<CredentialData>()), Times.Once);
|
||||||
|
Assert.False(hc.AllowAuthMigration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +307,7 @@ namespace GitHub.Runner.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async Task UpdateRunnerConfigAsync_RefreshRunnerCredetialsFailure_ShouldReportTelemetry()
|
public async Task UpdateRunnerConfigAsync_RefreshRunnerCredentialsFailure_ShouldReportTelemetry()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
@@ -510,6 +511,56 @@ namespace GitHub.Runner.Tests.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task UpdateRunnerConfigAsync_RefreshOAuthCredentialsWithDifferentAuthUrl_ShouldReportTelemetry()
|
||||||
|
{
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configurationStore.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" };
|
||||||
|
_configurationStore.Setup(x => x.GetSettings()).Returns(setting);
|
||||||
|
var credData = new CredentialData
|
||||||
|
{
|
||||||
|
Scheme = "OAuth"
|
||||||
|
};
|
||||||
|
credData.Data.Add("clientId", "12345");
|
||||||
|
credData.Data.Add("authorizationUrl", "http://example.com/");
|
||||||
|
_configurationStore.Setup(x => x.GetCredentials()).Returns(credData);
|
||||||
|
|
||||||
|
IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner));
|
||||||
|
IOUtil.SaveObject(credData, hc.GetConfigFile(WellKnownConfigFile.Credentials));
|
||||||
|
|
||||||
|
var differentCredData = new CredentialData
|
||||||
|
{
|
||||||
|
Scheme = "OAuth"
|
||||||
|
};
|
||||||
|
differentCredData.Data.Add("clientId", "12345");
|
||||||
|
differentCredData.Data.Add("authorizationUrl", "http://example2.com/");
|
||||||
|
var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(differentCredData)));
|
||||||
|
_runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny<int>(), It.Is<string>(s => s == "credentials"), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(encodedConfig);
|
||||||
|
|
||||||
|
var _runnerConfigUpdater = new RunnerConfigUpdater();
|
||||||
|
_runnerConfigUpdater.Initialize(hc);
|
||||||
|
|
||||||
|
var validRunnerQualifiedId = "valid/runner/qualifiedid/1";
|
||||||
|
var configType = "credentials";
|
||||||
|
var serviceType = "pipelines";
|
||||||
|
var configRefreshUrl = "http://example.com";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("Credential authorizationUrl in refreshed config")), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny<CredentialData>()), Times.Never);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
@@ -575,5 +626,53 @@ namespace GitHub.Runner.Tests.Listener
|
|||||||
_configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny<RunnerSettings>()), Times.Never);
|
_configurationStore.Verify(x => x.SaveMigratedSettings(It.IsAny<RunnerSettings>()), Times.Never);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task UpdateRunnerConfigAsync_UpdateRunnerCredentials_EnableDisableAuthMigration()
|
||||||
|
{
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configurationStore.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var setting = new RunnerSettings { AgentId = 1, AgentName = "agent1" };
|
||||||
|
_configurationStore.Setup(x => x.GetSettings()).Returns(setting);
|
||||||
|
var credData = new CredentialData
|
||||||
|
{
|
||||||
|
Scheme = "OAuth"
|
||||||
|
};
|
||||||
|
credData.Data.Add("ClientId", "12345");
|
||||||
|
credData.Data.Add("AuthorizationUrl", "https://example.com");
|
||||||
|
credData.Data.Add("AuthorizationUrlV2", "https://example2.com");
|
||||||
|
_configurationStore.Setup(x => x.GetCredentials()).Returns(credData);
|
||||||
|
|
||||||
|
IOUtil.SaveObject(setting, hc.GetConfigFile(WellKnownConfigFile.Runner));
|
||||||
|
IOUtil.SaveObject(credData, hc.GetConfigFile(WellKnownConfigFile.Credentials));
|
||||||
|
|
||||||
|
var encodedConfig = Convert.ToBase64String(Encoding.UTF8.GetBytes(StringUtil.ConvertToJson(credData)));
|
||||||
|
_runnerServer.Setup(x => x.RefreshRunnerConfigAsync(It.IsAny<int>(), It.Is<string>(s => s == "credentials"), It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(encodedConfig);
|
||||||
|
|
||||||
|
var _runnerConfigUpdater = new RunnerConfigUpdater();
|
||||||
|
_runnerConfigUpdater.Initialize(hc);
|
||||||
|
Assert.False(hc.AllowAuthMigration);
|
||||||
|
|
||||||
|
var validRunnerQualifiedId = "valid/runner/qualifiedid/1";
|
||||||
|
var configType = "credentials";
|
||||||
|
var serviceType = "pipelines";
|
||||||
|
var configRefreshUrl = "http://example.com";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _runnerConfigUpdater.UpdateRunnerConfigAsync(validRunnerQualifiedId, configType, serviceType, configRefreshUrl);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_runnerServer.Verify(x => x.RefreshRunnerConfigAsync(1, "credentials", It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("Runner credentials updated successfully")), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
_configurationStore.Verify(x => x.SaveMigratedCredential(It.IsAny<CredentialData>()), Times.Once);
|
||||||
|
Assert.True(hc.AllowAuthMigration);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
using GitHub.DistributedTask.WebApi;
|
using System;
|
||||||
using GitHub.Runner.Listener;
|
|
||||||
using GitHub.Runner.Listener.Configuration;
|
|
||||||
using Moq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Listener;
|
||||||
|
using GitHub.Runner.Listener.Configuration;
|
||||||
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests.Listener
|
namespace GitHub.Runner.Common.Tests.Listener
|
||||||
@@ -24,6 +26,9 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
private Mock<IConfigurationStore> _configStore;
|
private Mock<IConfigurationStore> _configStore;
|
||||||
private Mock<ISelfUpdater> _updater;
|
private Mock<ISelfUpdater> _updater;
|
||||||
private Mock<IErrorThrottler> _acquireJobThrottler;
|
private Mock<IErrorThrottler> _acquireJobThrottler;
|
||||||
|
private Mock<ICredentialManager> _credentialManager;
|
||||||
|
private Mock<IActionsRunServer> _actionsRunServer;
|
||||||
|
private Mock<IRunServer> _runServer;
|
||||||
|
|
||||||
public RunnerL0()
|
public RunnerL0()
|
||||||
{
|
{
|
||||||
@@ -37,6 +42,9 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
_configStore = new Mock<IConfigurationStore>();
|
_configStore = new Mock<IConfigurationStore>();
|
||||||
_updater = new Mock<ISelfUpdater>();
|
_updater = new Mock<ISelfUpdater>();
|
||||||
_acquireJobThrottler = new Mock<IErrorThrottler>();
|
_acquireJobThrottler = new Mock<IErrorThrottler>();
|
||||||
|
_credentialManager = new Mock<ICredentialManager>();
|
||||||
|
_actionsRunServer = new Mock<IActionsRunServer>();
|
||||||
|
_runServer = new Mock<IRunServer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName)
|
private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName)
|
||||||
@@ -57,7 +65,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
//process 2 new job messages, and one cancel message
|
//process 2 new job messages, and one cancel message
|
||||||
public async void TestRunAsync()
|
public async Task TestRunAsync()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
@@ -169,7 +177,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[MemberData(nameof(RunAsServiceTestData))]
|
[MemberData(nameof(RunAsServiceTestData))]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestExecuteCommandForRunAsService(string[] args, bool configureAsService, Times expectedTimes)
|
public async Task TestExecuteCommandForRunAsService(string[] args, bool configureAsService, Times expectedTimes)
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
@@ -177,6 +185,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
||||||
hc.SetSingleton<IMessageListener>(_messageListener.Object);
|
hc.SetSingleton<IMessageListener>(_messageListener.Object);
|
||||||
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||||
|
|
||||||
var command = new CommandSettings(hc, args);
|
var command = new CommandSettings(hc, args);
|
||||||
@@ -201,7 +210,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestMachineProvisionerCLI()
|
public async Task TestMachineProvisionerCLI()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
@@ -209,6 +218,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
||||||
hc.SetSingleton<IMessageListener>(_messageListener.Object);
|
hc.SetSingleton<IMessageListener>(_messageListener.Object);
|
||||||
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||||
|
|
||||||
var command = new CommandSettings(hc, new[] { "run" });
|
var command = new CommandSettings(hc, new[] { "run" });
|
||||||
@@ -235,7 +245,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestRunOnce()
|
public async Task TestRunOnce()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
@@ -332,7 +342,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestRunOnceOnlyTakeOneJobMessage()
|
public async Task TestRunOnceOnlyTakeOneJobMessage()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
@@ -433,7 +443,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestRunOnceHandleUpdateMessage()
|
public async Task TestRunOnceHandleUpdateMessage()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
@@ -523,13 +533,14 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
[Trait("Category", "Runner")]
|
||||||
public async void TestRemoveLocalRunnerConfig()
|
public async Task TestRemoveLocalRunnerConfig()
|
||||||
{
|
{
|
||||||
using (var hc = new TestHostContext(this))
|
using (var hc = new TestHostContext(this))
|
||||||
{
|
{
|
||||||
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
|
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
|
||||||
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||||
|
|
||||||
var command = new CommandSettings(hc, new[] { "remove", "--local" });
|
var command = new CommandSettings(hc, new[] { "remove", "--local" });
|
||||||
@@ -549,5 +560,521 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
_configurationManager.Verify(x => x.DeleteLocalRunnerConfig(), Times.Once());
|
_configurationManager.Verify(x => x.DeleteLocalRunnerConfig(), Times.Once());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task TestReportAuthMigrationTelemetry()
|
||||||
|
{
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
//Arrange
|
||||||
|
var runner = new Runner.Listener.Runner();
|
||||||
|
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
|
||||||
|
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
|
||||||
|
hc.SetSingleton<IMessageListener>(_messageListener.Object);
|
||||||
|
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
|
||||||
|
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||||
|
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||||
|
|
||||||
|
runner.Initialize(hc);
|
||||||
|
var settings = new RunnerSettings
|
||||||
|
{
|
||||||
|
PoolId = 43242,
|
||||||
|
AgentId = 5678,
|
||||||
|
Ephemeral = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var message1 = new TaskAgentMessage()
|
||||||
|
{
|
||||||
|
MessageId = 4234,
|
||||||
|
MessageType = "unknown"
|
||||||
|
};
|
||||||
|
|
||||||
|
var messages = new Queue<TaskAgentMessage>();
|
||||||
|
messages.Enqueue(message1);
|
||||||
|
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
_configurationManager.Setup(x => x.LoadSettings())
|
||||||
|
.Returns(settings);
|
||||||
|
_configurationManager.Setup(x => x.IsConfigured())
|
||||||
|
.Returns(true);
|
||||||
|
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
|
||||||
|
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (CancellationToken token) =>
|
||||||
|
{
|
||||||
|
hc.GetTrace().Info("Waiting for message");
|
||||||
|
Assert.False(hc.AllowAuthMigration);
|
||||||
|
await Task.Delay(100, token);
|
||||||
|
|
||||||
|
var traceFile = Path.GetTempFileName();
|
||||||
|
File.Copy(hc.TraceFileName, traceFile, true);
|
||||||
|
Assert.DoesNotContain("Checking for auth migration telemetry to report", File.ReadAllText(traceFile));
|
||||||
|
|
||||||
|
hc.EnableAuthMigration("L0Test");
|
||||||
|
hc.DeferAuthMigration(TimeSpan.FromSeconds(1), "L0Test");
|
||||||
|
hc.EnableAuthMigration("L0Test");
|
||||||
|
hc.DeferAuthMigration(TimeSpan.FromSeconds(1), "L0Test");
|
||||||
|
|
||||||
|
await Task.Delay(1000, token);
|
||||||
|
|
||||||
|
hc.ShutdownRunner(ShutdownReason.UserCancelled);
|
||||||
|
|
||||||
|
File.Copy(hc.TraceFileName, traceFile, true);
|
||||||
|
Assert.Contains("Checking for auth migration telemetry to report", File.ReadAllText(traceFile));
|
||||||
|
|
||||||
|
return messages.Dequeue();
|
||||||
|
});
|
||||||
|
_messageListener.Setup(x => x.DeleteSessionAsync())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
|
||||||
|
.Callback(() =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||||
|
|
||||||
|
_runnerServer.Setup(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(new TaskAgent()));
|
||||||
|
|
||||||
|
//Act
|
||||||
|
var command = new CommandSettings(hc, new string[] { "run" });
|
||||||
|
var returnCode = await runner.ExecuteCommand(command);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
Assert.Equal(Constants.Runner.ReturnCode.Success, returnCode);
|
||||||
|
|
||||||
|
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeastOnce());
|
||||||
|
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.Once());
|
||||||
|
|
||||||
|
_runnerServer.Verify(x => x.UpdateAgentUpdateStateAsync(It.IsAny<int>(), It.IsAny<ulong>(), It.IsAny<string>(), It.Is<string>(s => s.Contains("L0Test")), It.IsAny<CancellationToken>()), Times.Exactly(4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task TestRunnerJobRequestMessageFromPipeline()
|
||||||
|
{
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
//Arrange
|
||||||
|
var runner = new Runner.Listener.Runner();
|
||||||
|
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
|
||||||
|
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
|
||||||
|
hc.SetSingleton<IMessageListener>(_messageListener.Object);
|
||||||
|
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<ISelfUpdater>(_updater.Object);
|
||||||
|
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
|
||||||
|
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||||
|
hc.EnqueueInstance<IActionsRunServer>(_actionsRunServer.Object);
|
||||||
|
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||||
|
|
||||||
|
runner.Initialize(hc);
|
||||||
|
var settings = new RunnerSettings
|
||||||
|
{
|
||||||
|
PoolId = 43242,
|
||||||
|
AgentId = 5678,
|
||||||
|
Ephemeral = true,
|
||||||
|
ServerUrl = "https://github.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
var message1 = new TaskAgentMessage()
|
||||||
|
{
|
||||||
|
Body = JsonUtility.ToString(new RunnerJobRequestRef() { BillingOwnerId = "github", RunnerRequestId = "999" }),
|
||||||
|
MessageId = 4234,
|
||||||
|
MessageType = JobRequestMessageTypes.RunnerJobRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
var messages = new Queue<TaskAgentMessage>();
|
||||||
|
messages.Enqueue(message1);
|
||||||
|
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
_configurationManager.Setup(x => x.LoadSettings())
|
||||||
|
.Returns(settings);
|
||||||
|
_configurationManager.Setup(x => x.IsConfigured())
|
||||||
|
.Returns(true);
|
||||||
|
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
|
||||||
|
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (CancellationToken token) =>
|
||||||
|
{
|
||||||
|
if (0 == messages.Count)
|
||||||
|
{
|
||||||
|
await Task.Delay(2000, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.Dequeue();
|
||||||
|
});
|
||||||
|
_messageListener.Setup(x => x.DeleteSessionAsync())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
|
||||||
|
.Callback(() =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
_actionsRunServer.Setup(x => x.GetJobMessageAsync("999", It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(CreateJobRequestMessage("test")));
|
||||||
|
|
||||||
|
_credentialManager.Setup(x => x.LoadCredentials(false)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||||
|
|
||||||
|
var completedTask = new TaskCompletionSource<bool>();
|
||||||
|
completedTask.SetResult(true);
|
||||||
|
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
|
||||||
|
|
||||||
|
//Act
|
||||||
|
var command = new CommandSettings(hc, new string[] { "run" });
|
||||||
|
Task<int> runnerTask = runner.ExecuteCommand(command);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
//wait for the runner to exit with right return code
|
||||||
|
await Task.WhenAny(runnerTask, Task.Delay(30000));
|
||||||
|
|
||||||
|
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
|
||||||
|
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString());
|
||||||
|
if (runnerTask.IsCompleted)
|
||||||
|
{
|
||||||
|
Assert.Equal(Constants.Runner.ReturnCode.Success, await runnerTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeastOnce());
|
||||||
|
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.Once());
|
||||||
|
_credentialManager.Verify(x => x.LoadCredentials(false), Times.Once());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task TestRunnerJobRequestMessageFromRunService()
|
||||||
|
{
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
//Arrange
|
||||||
|
var runner = new Runner.Listener.Runner();
|
||||||
|
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
|
||||||
|
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
|
||||||
|
hc.SetSingleton<IMessageListener>(_messageListener.Object);
|
||||||
|
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<ISelfUpdater>(_updater.Object);
|
||||||
|
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
|
||||||
|
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||||
|
hc.EnqueueInstance<IRunServer>(_runServer.Object);
|
||||||
|
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||||
|
|
||||||
|
runner.Initialize(hc);
|
||||||
|
var settings = new RunnerSettings
|
||||||
|
{
|
||||||
|
PoolId = 43242,
|
||||||
|
AgentId = 5678,
|
||||||
|
Ephemeral = true,
|
||||||
|
ServerUrl = "https://github.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
var message1 = new TaskAgentMessage()
|
||||||
|
{
|
||||||
|
Body = JsonUtility.ToString(new RunnerJobRequestRef() { BillingOwnerId = "github", RunnerRequestId = "999", RunServiceUrl = "https://run-service.com" }),
|
||||||
|
MessageId = 4234,
|
||||||
|
MessageType = JobRequestMessageTypes.RunnerJobRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
var messages = new Queue<TaskAgentMessage>();
|
||||||
|
messages.Enqueue(message1);
|
||||||
|
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
_configurationManager.Setup(x => x.LoadSettings())
|
||||||
|
.Returns(settings);
|
||||||
|
_configurationManager.Setup(x => x.IsConfigured())
|
||||||
|
.Returns(true);
|
||||||
|
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
|
||||||
|
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (CancellationToken token) =>
|
||||||
|
{
|
||||||
|
if (0 == messages.Count)
|
||||||
|
{
|
||||||
|
await Task.Delay(2000, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.Dequeue();
|
||||||
|
});
|
||||||
|
_messageListener.Setup(x => x.DeleteSessionAsync())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
|
||||||
|
.Callback(() =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
_runServer.Setup(x => x.GetJobMessageAsync("999", "github", It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(CreateJobRequestMessage("test")));
|
||||||
|
|
||||||
|
_credentialManager.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||||
|
|
||||||
|
var completedTask = new TaskCompletionSource<bool>();
|
||||||
|
completedTask.SetResult(true);
|
||||||
|
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
|
||||||
|
|
||||||
|
//Act
|
||||||
|
var command = new CommandSettings(hc, new string[] { "run" });
|
||||||
|
Task<int> runnerTask = runner.ExecuteCommand(command);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
//wait for the runner to exit with right return code
|
||||||
|
await Task.WhenAny(runnerTask, Task.Delay(30000));
|
||||||
|
|
||||||
|
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
|
||||||
|
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString());
|
||||||
|
if (runnerTask.IsCompleted)
|
||||||
|
{
|
||||||
|
Assert.Equal(Constants.Runner.ReturnCode.Success, await runnerTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeastOnce());
|
||||||
|
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.Once());
|
||||||
|
_credentialManager.Verify(x => x.LoadCredentials(true), Times.Once());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Runner")]
|
||||||
|
public async Task TestRunnerJobRequestMessageFromRunService_AuthMigrationFallback()
|
||||||
|
{
|
||||||
|
using (var hc = new TestHostContext(this))
|
||||||
|
{
|
||||||
|
//Arrange
|
||||||
|
var runner = new Runner.Listener.Runner();
|
||||||
|
hc.SetSingleton<IConfigurationManager>(_configurationManager.Object);
|
||||||
|
hc.SetSingleton<IJobNotification>(_jobNotification.Object);
|
||||||
|
hc.SetSingleton<IMessageListener>(_messageListener.Object);
|
||||||
|
hc.SetSingleton<IPromptManager>(_promptManager.Object);
|
||||||
|
hc.SetSingleton<IRunnerServer>(_runnerServer.Object);
|
||||||
|
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||||
|
hc.SetSingleton<ISelfUpdater>(_updater.Object);
|
||||||
|
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
|
||||||
|
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||||
|
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||||
|
hc.EnqueueInstance<IRunServer>(_runServer.Object);
|
||||||
|
hc.EnqueueInstance<IRunServer>(_runServer.Object);
|
||||||
|
|
||||||
|
runner.Initialize(hc);
|
||||||
|
var settings = new RunnerSettings
|
||||||
|
{
|
||||||
|
PoolId = 43242,
|
||||||
|
AgentId = 5678,
|
||||||
|
Ephemeral = true,
|
||||||
|
ServerUrl = "https://github.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
var message1 = new TaskAgentMessage()
|
||||||
|
{
|
||||||
|
Body = JsonUtility.ToString(new RunnerJobRequestRef() { BillingOwnerId = "github", RunnerRequestId = "999", RunServiceUrl = "https://run-service.com" }),
|
||||||
|
MessageId = 4234,
|
||||||
|
MessageType = JobRequestMessageTypes.RunnerJobRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
var messages = new Queue<TaskAgentMessage>();
|
||||||
|
messages.Enqueue(message1);
|
||||||
|
messages.Enqueue(message1);
|
||||||
|
_updater.Setup(x => x.SelfUpdate(It.IsAny<AgentRefreshMessage>(), It.IsAny<IJobDispatcher>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
_configurationManager.Setup(x => x.LoadSettings())
|
||||||
|
.Returns(settings);
|
||||||
|
_configurationManager.Setup(x => x.IsConfigured())
|
||||||
|
.Returns(true);
|
||||||
|
_messageListener.Setup(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromResult<CreateSessionResult>(CreateSessionResult.Success));
|
||||||
|
_messageListener.Setup(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(async (CancellationToken token) =>
|
||||||
|
{
|
||||||
|
if (2 == messages.Count)
|
||||||
|
{
|
||||||
|
hc.EnableAuthMigration("L0Test");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 == messages.Count)
|
||||||
|
{
|
||||||
|
await Task.Delay(2000, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.Dequeue();
|
||||||
|
});
|
||||||
|
_messageListener.Setup(x => x.DeleteSessionAsync())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
|
||||||
|
.Callback(() =>
|
||||||
|
{
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var throwError = true;
|
||||||
|
_runServer.Setup(x => x.GetJobMessageAsync("999", "github", It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(() =>
|
||||||
|
{
|
||||||
|
if (throwError)
|
||||||
|
{
|
||||||
|
Assert.True(hc.AllowAuthMigration);
|
||||||
|
throwError = false;
|
||||||
|
throw new NotSupportedException("some error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(CreateJobRequestMessage("test"));
|
||||||
|
});
|
||||||
|
|
||||||
|
_credentialManager.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||||
|
|
||||||
|
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||||
|
|
||||||
|
var completedTask = new TaskCompletionSource<bool>();
|
||||||
|
completedTask.SetResult(true);
|
||||||
|
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
|
||||||
|
|
||||||
|
//Act
|
||||||
|
var command = new CommandSettings(hc, new string[] { "run" });
|
||||||
|
Task<int> runnerTask = runner.ExecuteCommand(command);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
//wait for the runner to exit with right return code
|
||||||
|
await Task.WhenAny(runnerTask, Task.Delay(30000));
|
||||||
|
|
||||||
|
Assert.True(runnerTask.IsCompleted, $"{nameof(runner.ExecuteCommand)} timed out.");
|
||||||
|
Assert.True(!runnerTask.IsFaulted, runnerTask.Exception?.ToString());
|
||||||
|
if (runnerTask.IsCompleted)
|
||||||
|
{
|
||||||
|
Assert.Equal(Constants.Runner.ReturnCode.Success, await runnerTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
_jobDispatcher.Verify(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), true), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.CreateSessionAsync(It.IsAny<CancellationToken>()), Times.Once());
|
||||||
|
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeast(2));
|
||||||
|
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.AtLeast(2));
|
||||||
|
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
|
||||||
|
_credentialManager.Verify(x => x.LoadCredentials(true), Times.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/Test/L0/Sdk/LaunchWebApi/LaunchHttpClientL0.cs
Normal file
126
src/Test/L0/Sdk/LaunchWebApi/LaunchHttpClientL0.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Test/L0/Sdk/RSWebApi/RunServiceHttpClientL0.cs
Normal file
20
src/Test/L0/Sdk/RSWebApi/RunServiceHttpClientL0.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
using GitHub.Runner.Common.Util;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.Loader;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Runtime.Loader;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
using GitHub.DistributedTask.Logging;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests
|
namespace GitHub.Runner.Common.Tests
|
||||||
@@ -31,6 +30,7 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
private StartupType _startupType;
|
private StartupType _startupType;
|
||||||
public event EventHandler Unloading;
|
public event EventHandler Unloading;
|
||||||
public event EventHandler<DelayEventArgs> Delaying;
|
public event EventHandler<DelayEventArgs> Delaying;
|
||||||
|
public event EventHandler<AuthMigrationEventArgs> AuthMigrationChanged;
|
||||||
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
|
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
|
||||||
public ShutdownReason RunnerShutdownReason { get; private set; }
|
public ShutdownReason RunnerShutdownReason { get; private set; }
|
||||||
public ISecretMasker SecretMasker => _secretMasker;
|
public ISecretMasker SecretMasker => _secretMasker;
|
||||||
@@ -92,6 +92,8 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
|
|
||||||
public RunnerWebProxy WebProxy => new();
|
public RunnerWebProxy WebProxy => new();
|
||||||
|
|
||||||
|
public bool AllowAuthMigration { get; set; }
|
||||||
|
|
||||||
public async Task Delay(TimeSpan delay, CancellationToken token)
|
public async Task Delay(TimeSpan delay, CancellationToken token)
|
||||||
{
|
{
|
||||||
// Event callback
|
// Event callback
|
||||||
@@ -101,8 +103,8 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
handler(this, new DelayEventArgs(delay, token));
|
handler(this, new DelayEventArgs(delay, token));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay zero
|
// Delay 10ms
|
||||||
await Task.Delay(TimeSpan.Zero);
|
await Task.Delay(TimeSpan.FromMilliseconds(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
public T CreateService<T>() where T : class, IRunnerService
|
public T CreateService<T>() where T : class, IRunnerService
|
||||||
@@ -387,6 +389,18 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void EnableAuthMigration(string trace)
|
||||||
|
{
|
||||||
|
AllowAuthMigration = true;
|
||||||
|
AuthMigrationChanged?.Invoke(this, new AuthMigrationEventArgs(trace));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeferAuthMigration(TimeSpan deferred, string trace)
|
||||||
|
{
|
||||||
|
AllowAuthMigration = false;
|
||||||
|
AuthMigrationChanged?.Invoke(this, new AuthMigrationEventArgs(trace));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DelayEventArgs : EventArgs
|
public class DelayEventArgs : EventArgs
|
||||||
|
|||||||
120
src/Test/L0/Util/NodeUtilL0.cs
Normal file
120
src/Test/L0/Util/NodeUtilL0.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1659,6 +1659,76 @@ runs:
|
|||||||
Teardown();
|
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]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
@@ -2411,8 +2481,8 @@ runs:
|
|||||||
});
|
});
|
||||||
|
|
||||||
_launchServer = new Mock<ILaunchServer>();
|
_launchServer = new Mock<ILaunchServer>();
|
||||||
_launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<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) =>
|
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors) =>
|
||||||
{
|
{
|
||||||
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
|
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
|
||||||
foreach (var action in actions.Actions)
|
foreach (var action in actions.Actions)
|
||||||
|
|||||||
@@ -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]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
@@ -758,7 +801,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
//Assert
|
//Assert
|
||||||
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
|
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
|
||||||
Assert.Contains($"Failed to load {action_path}", err.Message);
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1168,6 +1168,77 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void InitializeJob_HydratesJobContextWithCheckRunId()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
// Arrange: Create a job request message and make sure the feature flag is enabled
|
||||||
|
var variables = new Dictionary<string, VariableValue>()
|
||||||
|
{
|
||||||
|
[Constants.Runner.Features.AddCheckRunIdToJobContext] = new VariableValue("true"),
|
||||||
|
};
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
|
||||||
|
var pagingLogger = new Moq.Mock<IPagingLogger>();
|
||||||
|
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
|
||||||
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
|
||||||
|
// Arrange: Add check_run_id to the job context
|
||||||
|
var jobContext = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
jobContext["check_run_id"] = new NumberContextData(123456);
|
||||||
|
jobRequest.ContextData["job"] = jobContext;
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(ec.JobContext);
|
||||||
|
Assert.Equal(123456, ec.JobContext.CheckRunId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this test can be deleted when `AddCheckRunIdToJobContext` is fully rolled out
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void InitializeJob_HydratesJobContextWithCheckRunId_FeatureFlagDisabled()
|
||||||
|
{
|
||||||
|
using (TestHostContext hc = CreateTestContext())
|
||||||
|
{
|
||||||
|
// Arrange: Create a job request message and make sure the feature flag is disabled
|
||||||
|
var variables = new Dictionary<string, VariableValue>()
|
||||||
|
{
|
||||||
|
[Constants.Runner.Features.AddCheckRunIdToJobContext] = new VariableValue("false"),
|
||||||
|
};
|
||||||
|
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
|
||||||
|
var pagingLogger = new Moq.Mock<IPagingLogger>();
|
||||||
|
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
|
||||||
|
hc.EnqueueInstance(pagingLogger.Object);
|
||||||
|
hc.SetSingleton(jobServerQueue.Object);
|
||||||
|
var ec = new Runner.Worker.ExecutionContext();
|
||||||
|
ec.Initialize(hc);
|
||||||
|
|
||||||
|
// Arrange: Add check_run_id to the job context
|
||||||
|
var jobContext = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
jobContext["check_run_id"] = new NumberContextData(123456);
|
||||||
|
jobRequest.ContextData["job"] = jobContext;
|
||||||
|
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(ec.JobContext);
|
||||||
|
Assert.Null(ec.JobContext.CheckRunId); // with the feature flag disabled we should not have added a CheckRunId to the JobContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool ExpressionValuesAssertEqual(DictionaryContextData expect, DictionaryContextData actual)
|
private bool ExpressionValuesAssertEqual(DictionaryContextData expect, DictionaryContextData actual)
|
||||||
{
|
{
|
||||||
foreach (var key in expect.Keys.ToList())
|
foreach (var key in expect.Keys.ToList())
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
[InlineData("node12", "node20")]
|
[InlineData("node12", "node20")]
|
||||||
[InlineData("node16", "node20")]
|
[InlineData("node16", "node20")]
|
||||||
[InlineData("node20", "node20")]
|
[InlineData("node20", "node20")]
|
||||||
|
[InlineData("node24", "node24")]
|
||||||
public void IsNodeVersionUpgraded(string inputVersion, string expectedVersion)
|
public void IsNodeVersionUpgraded(string inputVersion, string expectedVersion)
|
||||||
{
|
{
|
||||||
using (TestHostContext hc = CreateTestContext())
|
using (TestHostContext hc = CreateTestContext())
|
||||||
@@ -41,7 +42,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
var hf = new HandlerFactory();
|
var hf = new HandlerFactory();
|
||||||
hf.Initialize(hc);
|
hf.Initialize(hc);
|
||||||
|
|
||||||
// Server Feature Flag
|
// Setup variables
|
||||||
var variables = new Dictionary<string, VariableValue>();
|
var variables = new Dictionary<string, VariableValue>();
|
||||||
Variables serverVariables = new(hc, variables);
|
Variables serverVariables = new(hc, variables);
|
||||||
|
|
||||||
@@ -72,5 +73,48 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
Assert.Equal(expectedVersion, handler.Data.NodeVersion);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/Test/L0/Worker/Handlers/NodeHandlerL0.cs
Normal file
35
src/Test/L0/Worker/Handlers/NodeHandlerL0.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -896,5 +896,173 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
Assert.Equal("not-working", match.Message);
|
Assert.Equal("not-working", match.Message);
|
||||||
Assert.Equal("my-project.proj", match.FromPath);
|
Assert.Equal("my-project.proj", match.FromPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void Matcher_SinglePattern_DefaultFromPath()
|
||||||
|
{
|
||||||
|
var config = JsonUtility.FromString<IssueMatchersConfig>(@"
|
||||||
|
{
|
||||||
|
""problemMatcher"": [
|
||||||
|
{
|
||||||
|
""owner"": ""myMatcher"",
|
||||||
|
""fromPath"": ""subdir/default-project.csproj"",
|
||||||
|
""pattern"": [
|
||||||
|
{
|
||||||
|
""regexp"": ""^file:(.+) line:(.+) column:(.+) severity:(.+) code:(.+) message:(.+)$"",
|
||||||
|
""file"": 1,
|
||||||
|
""line"": 2,
|
||||||
|
""column"": 3,
|
||||||
|
""severity"": 4,
|
||||||
|
""code"": 5,
|
||||||
|
""message"": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
");
|
||||||
|
config.Validate();
|
||||||
|
var matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
var match = matcher.Match("file:my-file.cs line:123 column:45 severity:real-bad code:uh-oh message:not-working");
|
||||||
|
Assert.Equal("my-file.cs", match.File);
|
||||||
|
Assert.Equal("123", match.Line);
|
||||||
|
Assert.Equal("45", match.Column);
|
||||||
|
Assert.Equal("real-bad", match.Severity);
|
||||||
|
Assert.Equal("uh-oh", match.Code);
|
||||||
|
Assert.Equal("not-working", match.Message);
|
||||||
|
Assert.Equal("subdir/default-project.csproj", match.FromPath);
|
||||||
|
|
||||||
|
// Test that a pattern-specific fromPath overrides the default
|
||||||
|
config = JsonUtility.FromString<IssueMatchersConfig>(@"
|
||||||
|
{
|
||||||
|
""problemMatcher"": [
|
||||||
|
{
|
||||||
|
""owner"": ""myMatcher"",
|
||||||
|
""fromPath"": ""subdir/default-project.csproj"",
|
||||||
|
""pattern"": [
|
||||||
|
{
|
||||||
|
""regexp"": ""^file:(.+) line:(.+) column:(.+) severity:(.+) code:(.+) message:(.+) fromPath:(.+)$"",
|
||||||
|
""file"": 1,
|
||||||
|
""line"": 2,
|
||||||
|
""column"": 3,
|
||||||
|
""severity"": 4,
|
||||||
|
""code"": 5,
|
||||||
|
""message"": 6,
|
||||||
|
""fromPath"": 7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
");
|
||||||
|
config.Validate();
|
||||||
|
matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
match = matcher.Match("file:my-file.cs line:123 column:45 severity:real-bad code:uh-oh message:not-working fromPath:my-project.proj");
|
||||||
|
Assert.Equal("my-file.cs", match.File);
|
||||||
|
Assert.Equal("123", match.Line);
|
||||||
|
Assert.Equal("45", match.Column);
|
||||||
|
Assert.Equal("real-bad", match.Severity);
|
||||||
|
Assert.Equal("uh-oh", match.Code);
|
||||||
|
Assert.Equal("not-working", match.Message);
|
||||||
|
Assert.Equal("my-project.proj", match.FromPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void Matcher_MultiplePatterns_DefaultFromPath()
|
||||||
|
{
|
||||||
|
var config = JsonUtility.FromString<IssueMatchersConfig>(@"
|
||||||
|
{
|
||||||
|
""problemMatcher"": [
|
||||||
|
{
|
||||||
|
""owner"": ""myMatcher"",
|
||||||
|
""fromPath"": ""subdir/default-project.csproj"",
|
||||||
|
""pattern"": [
|
||||||
|
{
|
||||||
|
""regexp"": ""^file:(.+)$"",
|
||||||
|
""file"": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""regexp"": ""^severity:(.+)$"",
|
||||||
|
""severity"": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""regexp"": ""^line:(.+) column:(.+) code:(.+) message:(.+)$"",
|
||||||
|
""line"": 1,
|
||||||
|
""column"": 2,
|
||||||
|
""code"": 3,
|
||||||
|
""message"": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
");
|
||||||
|
config.Validate();
|
||||||
|
var matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
var match = matcher.Match("file:my-file.cs");
|
||||||
|
Assert.Null(match);
|
||||||
|
match = matcher.Match("severity:real-bad");
|
||||||
|
Assert.Null(match);
|
||||||
|
match = matcher.Match("line:123 column:45 code:uh-oh message:not-working");
|
||||||
|
Assert.Equal("my-file.cs", match.File);
|
||||||
|
Assert.Equal("123", match.Line);
|
||||||
|
Assert.Equal("45", match.Column);
|
||||||
|
Assert.Equal("real-bad", match.Severity);
|
||||||
|
Assert.Equal("uh-oh", match.Code);
|
||||||
|
Assert.Equal("not-working", match.Message);
|
||||||
|
Assert.Equal("subdir/default-project.csproj", match.FromPath);
|
||||||
|
|
||||||
|
// Test that pattern-specific fromPath overrides the default
|
||||||
|
config = JsonUtility.FromString<IssueMatchersConfig>(@"
|
||||||
|
{
|
||||||
|
""problemMatcher"": [
|
||||||
|
{
|
||||||
|
""owner"": ""myMatcher"",
|
||||||
|
""fromPath"": ""subdir/default-project.csproj"",
|
||||||
|
""pattern"": [
|
||||||
|
{
|
||||||
|
""regexp"": ""^file:(.+) fromPath:(.+)$"",
|
||||||
|
""file"": 1,
|
||||||
|
""fromPath"": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""regexp"": ""^severity:(.+)$"",
|
||||||
|
""severity"": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""regexp"": ""^line:(.+) column:(.+) code:(.+) message:(.+)$"",
|
||||||
|
""line"": 1,
|
||||||
|
""column"": 2,
|
||||||
|
""code"": 3,
|
||||||
|
""message"": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
");
|
||||||
|
config.Validate();
|
||||||
|
matcher = new IssueMatcher(config.Matchers[0], TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
match = matcher.Match("file:my-file.cs fromPath:my-project.proj");
|
||||||
|
Assert.Null(match);
|
||||||
|
match = matcher.Match("severity:real-bad");
|
||||||
|
Assert.Null(match);
|
||||||
|
match = matcher.Match("line:123 column:45 code:uh-oh message:not-working");
|
||||||
|
Assert.Equal("my-file.cs", match.File);
|
||||||
|
Assert.Equal("123", match.Line);
|
||||||
|
Assert.Equal("45", match.Column);
|
||||||
|
Assert.Equal("real-bad", match.Severity);
|
||||||
|
Assert.Equal("uh-oh", match.Code);
|
||||||
|
Assert.Equal("not-working", match.Message);
|
||||||
|
Assert.Equal("my-project.proj", match.FromPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/Test/L0/Worker/JobContextL0.cs
Normal file
38
src/Test/L0/Worker/JobContextL0.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -937,6 +937,62 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async void MatcherDefaultFromPath()
|
||||||
|
{
|
||||||
|
var matchers = new IssueMatchersConfig
|
||||||
|
{
|
||||||
|
Matchers =
|
||||||
|
{
|
||||||
|
new IssueMatcherConfig
|
||||||
|
{
|
||||||
|
Owner = "my-matcher-1",
|
||||||
|
FromPath = "workflow-repo/some-project/some-project.proj",
|
||||||
|
Patterns = new[]
|
||||||
|
{
|
||||||
|
new IssuePatternConfig
|
||||||
|
{
|
||||||
|
Pattern = @"(.+): (.+)",
|
||||||
|
File = 1,
|
||||||
|
Message = 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
using (var hostContext = Setup(matchers: matchers))
|
||||||
|
using (_outputManager)
|
||||||
|
{
|
||||||
|
// Setup github.workspace, github.repository
|
||||||
|
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
|
||||||
|
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
|
||||||
|
Directory.CreateDirectory(workDirectory);
|
||||||
|
var workspaceDirectory = Path.Combine(workDirectory, "workspace");
|
||||||
|
Directory.CreateDirectory(workspaceDirectory);
|
||||||
|
_executionContext.Setup(x => x.GetGitHubContext("workspace")).Returns(workspaceDirectory);
|
||||||
|
_executionContext.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/workflow-repo");
|
||||||
|
|
||||||
|
// Setup a git repository
|
||||||
|
var repositoryPath = Path.Combine(workspaceDirectory, "workflow-repo");
|
||||||
|
await CreateRepository(hostContext, repositoryPath, "https://github.com/my-org/workflow-repo");
|
||||||
|
|
||||||
|
// Create a test file
|
||||||
|
var filePath = Path.Combine(repositoryPath, "some-project", "some-directory", "some-file.txt");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
||||||
|
File.WriteAllText(filePath, "");
|
||||||
|
|
||||||
|
// Process
|
||||||
|
Process("some-directory/some-file.txt: some error");
|
||||||
|
Assert.Equal(1, _issues.Count);
|
||||||
|
Assert.Equal("some error", _issues[0].Item1.Message);
|
||||||
|
Assert.Equal("some-project/some-directory/some-file.txt", _issues[0].Item1.Data["file"]);
|
||||||
|
Assert.Equal(0, _commands.Count);
|
||||||
|
Assert.Equal(0, _messages.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
|
|||||||
@@ -162,6 +162,60 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
Assert.Equal("node20", nodeVersion);
|
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
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
src/Test/L0/Worker/StepHostNodeVersionL0.cs
Normal file
63
src/Test/L0/Worker/StepHostNodeVersionL0.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageReference Include="xunit" Version="2.7.1" />
|
<PackageReference Include="xunit" Version="2.7.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||||
<PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" />
|
<PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" />
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
|||||||
20
src/Test/TestData/node24action.yml
Normal file
20
src/Test/TestData/node24action.yml
Normal 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'
|
||||||
@@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout"
|
|||||||
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
|
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
|
||||||
PACKAGE_DIR="$SCRIPT_DIR/../_package"
|
PACKAGE_DIR="$SCRIPT_DIR/../_package"
|
||||||
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
|
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
|
||||||
DOTNETSDK_VERSION="8.0.407"
|
DOTNETSDK_VERSION="8.0.412"
|
||||||
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
|
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
|
||||||
RUNNER_VERSION=$(cat runnerversion)
|
RUNNER_VERSION=$(cat runnerversion)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "8.0.407"
|
"version": "8.0.412"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.323.0
|
2.328.0
|
||||||
|
|||||||
Reference in New Issue
Block a user