mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:36:23 +00:00
Compare commits
41 Commits
users/tihu
...
salmanmkc/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53a21f1a10 | ||
|
|
284c8ea43c | ||
|
|
1724385ca1 | ||
|
|
0bc856255b | ||
|
|
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 |
@@ -4,7 +4,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
|
||||
"ghcr.io/devcontainers/features/dotnet": {
|
||||
"version": "8.0.408"
|
||||
"version": "8.0.412"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"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).
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
devScript: ./dev.sh
|
||||
|
||||
- runtime: win-x64
|
||||
os: windows-2019
|
||||
os: windows-latest
|
||||
devScript: ./dev
|
||||
|
||||
- runtime: win-arm64
|
||||
|
||||
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@v4
|
||||
|
||||
- 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@v4
|
||||
|
||||
- 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"
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
devScript: ./dev.sh
|
||||
|
||||
- runtime: win-x64
|
||||
os: windows-2019
|
||||
os: windows-latest
|
||||
devScript: ./dev
|
||||
|
||||
- runtime: win-arm64
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
|
||||
# Upload release assets (full runner packages)
|
||||
- name: Upload Release Asset (win-x64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (win-arm64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (linux-x64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -244,7 +244,7 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (osx-x64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (osx-arm64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (linux-arm)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -274,7 +274,7 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (linux-arm64)
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
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)
|
||||
|
||||
## 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)
|
||||
|
||||
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.
|
||||
|
||||
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`
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG RUNNER_VERSION
|
||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||
ARG DOCKER_VERSION=28.0.1
|
||||
ARG BUILDX_VERSION=0.21.2
|
||||
ARG DOCKER_VERSION=28.3.2
|
||||
ARG BUILDX_VERSION=0.26.1
|
||||
|
||||
RUN apt update -y && apt install curl unzip -y
|
||||
|
||||
|
||||
@@ -1,36 +1,13 @@
|
||||
## What's Changed
|
||||
* Bump docker/login-action from 2 to 3 by @dependabot in https://github.com/actions/runner/pull/3673
|
||||
* Bump actions/stale from 8 to 9 by @dependabot in https://github.com/actions/runner/pull/3554
|
||||
* Bump docker/build-push-action from 3 to 6 by @dependabot in https://github.com/actions/runner/pull/3674
|
||||
* update node version from 20.18.0 -> 20.18.2 by @aiqiaoy in https://github.com/actions/runner/pull/3682
|
||||
* Pass BillingOwnerId through Acquire/Complete calls by @luketomlinson in https://github.com/actions/runner/pull/3689
|
||||
* Do not retry CompleteJobAsync for known non-retryable errors by @ericsciple in https://github.com/actions/runner/pull/3696
|
||||
* Update dotnet sdk to latest version @8.0.406 by @github-actions in https://github.com/actions/runner/pull/3712
|
||||
* Update Dockerfile with new docker and buildx versions by @thboop in https://github.com/actions/runner/pull/3680
|
||||
* chore: remove redundant words by @finaltrip in https://github.com/actions/runner/pull/3705
|
||||
* fix: actions feedback link is incorrect by @Yaminyam in https://github.com/actions/runner/pull/3165
|
||||
* Bump actions/github-script from 0.3.0 to 7.0.1 by @dependabot in https://github.com/actions/runner/pull/3557
|
||||
* Docker container provenance by @paveliak in https://github.com/actions/runner/pull/3736
|
||||
* Add request-id to http eventsource trace. by @TingluoHuang in https://github.com/actions/runner/pull/3740
|
||||
* Update Bocker and Buildx version to mitigate images scanners alerts by @Blizter in https://github.com/actions/runner/pull/3750
|
||||
* Fix typo, add invariant culture to timestamp for workflow log reporting by @GhadimiR in https://github.com/actions/runner/pull/3749
|
||||
* Create vssconnection to actions service when URL provided. by @TingluoHuang in https://github.com/actions/runner/pull/3751
|
||||
* Housekeeping: Update npm packages and node version by @thboop in https://github.com/actions/runner/pull/3752
|
||||
* Improve the out-of-date warning message. by @tecimovic in https://github.com/actions/runner/pull/3595
|
||||
* Update dotnet sdk to latest version @8.0.407 by @github-actions in https://github.com/actions/runner/pull/3753
|
||||
* Exit hosted runner cleanly during deprovisioning. by @TingluoHuang in https://github.com/actions/runner/pull/3755
|
||||
* Send annotation title to run-service. by @TingluoHuang in https://github.com/actions/runner/pull/3757
|
||||
* Allow server enforce runner settings. by @TingluoHuang in https://github.com/actions/runner/pull/3758
|
||||
* Support refresh runner configs with pipelines service. by @TingluoHuang in https://github.com/actions/runner/pull/3706
|
||||
* Try add orchestrationid into user-agent using token claim. by @TingluoHuang in https://github.com/actions/runner/pull/3945
|
||||
* Fix null reference exception in user agent handling by @salmanmkc in https://github.com/actions/runner/pull/3946
|
||||
* Runner Support for executing Node24 Actions by @salmanmkc in https://github.com/actions/runner/pull/3940
|
||||
* Update dotnet sdk to latest version @8.0.412 by @github-actions[bot] in https://github.com/actions/runner/pull/3941
|
||||
|
||||
## New Contributors
|
||||
* @finaltrip made their first contribution in https://github.com/actions/runner/pull/3705
|
||||
* @Yaminyam made their first contribution in https://github.com/actions/runner/pull/3165
|
||||
* @Blizter made their first contribution in https://github.com/actions/runner/pull/3750
|
||||
* @GhadimiR made their first contribution in https://github.com/actions/runner/pull/3749
|
||||
* @tecimovic made their first contribution in https://github.com/actions/runner/pull/3595
|
||||
* @salmanmkc made their first contribution in https://github.com/actions/runner/pull/3946
|
||||
|
||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.322.0...v2.323.0
|
||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.326.0...v2.327.0
|
||||
|
||||
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
||||
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
||||
|
||||
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": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -4751,9 +4752,9 @@
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"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
|
||||
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
||||
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
||||
NODE20_VERSION="20.19.0"
|
||||
NODE20_VERSION="20.19.3"
|
||||
NODE24_VERSION="24.4.0"
|
||||
|
||||
get_abs_path() {
|
||||
# exploits the fact that pwd will print abs path when no args
|
||||
@@ -139,6 +140,8 @@ function acquireExternalTool() {
|
||||
if [[ "$PACKAGERUNTIME" == "win-x64" || "$PACKAGERUNTIME" == "win-x86" ]]; then
|
||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
|
||||
if [[ "$PRECACHE" != "" ]]; then
|
||||
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
|
||||
fi
|
||||
@@ -149,6 +152,8 @@ if [[ "$PACKAGERUNTIME" == "win-arm64" ]]; then
|
||||
# todo: replace these with official release when available
|
||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.exe" node20/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/$PACKAGERUNTIME/node.lib" node20/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.exe" node24/bin
|
||||
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/$PACKAGERUNTIME/node.lib" node24/bin
|
||||
if [[ "$PRECACHE" != "" ]]; then
|
||||
acquireExternalTool "https://github.com/microsoft/vswhere/releases/download/2.6.7/vswhere.exe" vswhere
|
||||
fi
|
||||
@@ -157,21 +162,26 @@ fi
|
||||
# Download the external tools only for OSX.
|
||||
if [[ "$PACKAGERUNTIME" == "osx-x64" ]]; then
|
||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-x64.tar.gz" node20 fix_nested_dir
|
||||
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-x64.tar.gz" node24 fix_nested_dir
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGERUNTIME" == "osx-arm64" ]]; then
|
||||
# node.js v12 doesn't support macOS on arm64.
|
||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-darwin-arm64.tar.gz" node20 fix_nested_dir
|
||||
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-darwin-arm64.tar.gz" node24 fix_nested_dir
|
||||
fi
|
||||
|
||||
# Download the external tools for Linux PACKAGERUNTIMEs.
|
||||
if [[ "$PACKAGERUNTIME" == "linux-x64" ]]; then
|
||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-x64.tar.gz" node20 fix_nested_dir
|
||||
acquireExternalTool "$NODE_ALPINE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-alpine-x64.tar.gz" node20_alpine
|
||||
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-x64.tar.gz" node24 fix_nested_dir
|
||||
acquireExternalTool "$NODE_ALPINE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-alpine-x64.tar.gz" node24_alpine
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGERUNTIME" == "linux-arm64" ]]; then
|
||||
acquireExternalTool "$NODE_URL/v${NODE20_VERSION}/node-v${NODE20_VERSION}-linux-arm64.tar.gz" node20 fix_nested_dir
|
||||
acquireExternalTool "$NODE_URL/v${NODE24_VERSION}/node-v${NODE24_VERSION}-linux-arm64.tar.gz" node24 fix_nested_dir
|
||||
fi
|
||||
|
||||
if [[ "$PACKAGERUNTIME" == "linux-arm" ]]; then
|
||||
|
||||
@@ -3299,7 +3299,7 @@ function expand(str, isTop) {
|
||||
var isOptions = m.body.indexOf(',') >= 0;
|
||||
if (!isSequence && !isOptions) {
|
||||
// {a},b}
|
||||
if (m.post.match(/,.*\}/)) {
|
||||
if (m.post.match(/,(?!,).*\}/)) {
|
||||
str = m.pre + '{' + m.body + escClose + m.post;
|
||||
return expand(str);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,8 @@ fi
|
||||
# fix upgrade issue with macOS when running as a service
|
||||
attemptedtargetedfix=0
|
||||
currentplatform=$(uname | awk '{print tolower($0)}')
|
||||
if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
||||
if [[ "$currentplatform" == 'darwin' && $restartinteractiverunner -eq 0 ]];
|
||||
then
|
||||
# We needed a fix for https://github.com/actions/runner/issues/743
|
||||
# We will recreate the ./externals/nodeXY/bin/node of the past runner version that launched the runnerlistener service
|
||||
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
|
||||
@@ -135,16 +136,22 @@ if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
|
||||
then
|
||||
# inspect the open file handles to find the node process
|
||||
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
|
||||
nodever="node20"
|
||||
# Try finding node24 first, then fallback to earlier versions if needed
|
||||
nodever="node24"
|
||||
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
|
||||
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
|
||||
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node20
|
||||
then
|
||||
nodever="node16"
|
||||
nodever="node20"
|
||||
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
|
||||
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node12
|
||||
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node16
|
||||
then
|
||||
nodever="node12"
|
||||
nodever="node16"
|
||||
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
|
||||
if [[ $? -ne 0 || -z "$path" ]] # Fallback if RunnerService.js was started with node12
|
||||
then
|
||||
nodever="node12"
|
||||
path=$(lsof -a -g "$procgroup" -F n | grep $nodever/bin/node | grep externals | tail -1 | cut -c2-)
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [[ $? -eq 0 && -n "$path" ]]
|
||||
|
||||
@@ -116,6 +116,7 @@ namespace GitHub.Runner.Common
|
||||
bool IsConfigured();
|
||||
bool IsServiceConfigured();
|
||||
bool HasCredentials();
|
||||
bool IsMigratedConfigured();
|
||||
CredentialData GetCredentials();
|
||||
CredentialData GetMigratedCredentials();
|
||||
RunnerSettings GetSettings();
|
||||
@@ -198,6 +199,14 @@ namespace GitHub.Runner.Common
|
||||
return serviceConfigured;
|
||||
}
|
||||
|
||||
public bool IsMigratedConfigured()
|
||||
{
|
||||
Trace.Info("IsMigratedConfigured()");
|
||||
bool configured = new FileInfo(_migratedConfigFilePath).Exists;
|
||||
Trace.Info("IsMigratedConfigured: {0}", configured);
|
||||
return configured;
|
||||
}
|
||||
|
||||
public CredentialData GetCredentials()
|
||||
{
|
||||
if (_creds == null)
|
||||
|
||||
@@ -155,6 +155,10 @@ namespace GitHub.Runner.Common
|
||||
public const int RunnerUpdating = 3;
|
||||
public const int RunOnceRunnerUpdating = 4;
|
||||
public const int SessionConflict = 5;
|
||||
// Temporary error code to indicate that the runner configuration has been refreshed
|
||||
// and the runner should be restarted. This is a temporary code and will be removed in the future after
|
||||
// the runner is migrated to runner admin.
|
||||
public const int RunnerConfigurationRefreshed = 6;
|
||||
}
|
||||
|
||||
public static class Features
|
||||
@@ -163,6 +167,8 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string LogTemplateErrorsAsDebugMessages = "DistributedTask.LogTemplateErrorsAsDebugMessages";
|
||||
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
|
||||
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
|
||||
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
|
||||
public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors";
|
||||
}
|
||||
|
||||
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
||||
@@ -257,7 +263,6 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
|
||||
public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT";
|
||||
public static readonly string ActionArchiveCacheDirectory = "ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE";
|
||||
public static readonly string ActionsTerminationGracePeriodSeconds = "ACTIONS_RUNNER_TERMINATION_GRACE_PERIOD_SECONDS";
|
||||
}
|
||||
|
||||
public static class System
|
||||
|
||||
@@ -15,6 +15,7 @@ using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Logging;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.WebApi.Jwt;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
@@ -34,7 +35,7 @@ namespace GitHub.Runner.Common
|
||||
T GetService<T>() where T : class, IRunnerService;
|
||||
void SetDefaultCulture(string name);
|
||||
event EventHandler Unloading;
|
||||
void ShutdownRunner(ShutdownReason reason, TimeSpan delay = default);
|
||||
void ShutdownRunner(ShutdownReason reason);
|
||||
void WritePerfCounter(string counter);
|
||||
void LoadDefaultUserAgents();
|
||||
|
||||
@@ -74,8 +75,6 @@ namespace GitHub.Runner.Common
|
||||
private string _perfFile;
|
||||
private RunnerWebProxy _webProxy = new();
|
||||
private string _hostType = string.Empty;
|
||||
private ShutdownReason _shutdownReason = ShutdownReason.UserCancelled;
|
||||
private int _shutdownReasonSet = 0;
|
||||
|
||||
// disable auth migration by default
|
||||
private readonly ManualResetEventSlim _allowAuthMigration = new ManualResetEventSlim(false);
|
||||
@@ -87,7 +86,7 @@ namespace GitHub.Runner.Common
|
||||
public event EventHandler Unloading;
|
||||
public event EventHandler<AuthMigrationEventArgs> AuthMigrationChanged;
|
||||
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
|
||||
public ShutdownReason RunnerShutdownReason => _shutdownReason;
|
||||
public ShutdownReason RunnerShutdownReason { get; private set; }
|
||||
public ISecretMasker SecretMasker => _secretMasker;
|
||||
public List<ProductInfoHeaderValue> UserAgents => _userAgents;
|
||||
public RunnerWebProxy WebProxy => _webProxy;
|
||||
@@ -308,6 +307,36 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
_userAgents.Add(new ProductInfoHeaderValue("ClientId", clientId));
|
||||
}
|
||||
|
||||
// for Hosted runner, we can pull orchestrationId from JWT claims of the runner listening token.
|
||||
if (credData != null &&
|
||||
credData.Scheme == Constants.Configuration.OAuthAccessToken &&
|
||||
credData.Data.TryGetValue(Constants.Runner.CommandLine.Args.Token, out var accessToken) &&
|
||||
!string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var jwt = JsonWebToken.Create(accessToken);
|
||||
var claims = jwt.ExtractClaims();
|
||||
var orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orch_id", StringComparison.OrdinalIgnoreCase))?.Value;
|
||||
if (string.IsNullOrEmpty(orchestrationId))
|
||||
{
|
||||
// fallback to orchid for C# actions-service
|
||||
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(orchestrationId))
|
||||
{
|
||||
_trace.Info($"Pull OrchestrationId {orchestrationId} from runner JWT claims");
|
||||
_userAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", orchestrationId));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_trace.Error("Fail to extract OrchestrationId from runner JWT claims");
|
||||
_trace.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var runnerFile = GetConfigFile(WellKnownConfigFile.Runner);
|
||||
@@ -575,28 +604,12 @@ namespace GitHub.Runner.Common
|
||||
}
|
||||
|
||||
|
||||
public void ShutdownRunner(ShutdownReason reason, TimeSpan delay = default)
|
||||
public void ShutdownRunner(ShutdownReason reason)
|
||||
{
|
||||
ArgUtil.NotNull(reason, nameof(reason));
|
||||
_trace.Info($"Runner will be shutdown for {reason.ToString()} after {delay.TotalSeconds} seconds.");
|
||||
if (Interlocked.CompareExchange(ref _shutdownReasonSet, 1, 0) == 0)
|
||||
{
|
||||
// Set the shutdown reason only if it hasn't been set before.
|
||||
_shutdownReason = reason;
|
||||
}
|
||||
else
|
||||
{
|
||||
_trace.Verbose($"Runner shutdown reason already set to {_shutdownReason.ToString()}.");
|
||||
}
|
||||
|
||||
if (delay.TotalSeconds == 0)
|
||||
{
|
||||
_runnerShutdownTokenSource.Cancel();
|
||||
}
|
||||
else
|
||||
{
|
||||
_runnerShutdownTokenSource.CancelAfter(delay);
|
||||
}
|
||||
_trace.Info($"Runner will be shutdown for {reason.ToString()}");
|
||||
RunnerShutdownReason = reason;
|
||||
_runnerShutdownTokenSource.Cancel();
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
void InitializeLaunchClient(Uri uri, string token);
|
||||
|
||||
Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken);
|
||||
Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors);
|
||||
}
|
||||
|
||||
public sealed class LaunchServer : RunnerService, ILaunchServer
|
||||
@@ -42,12 +42,16 @@ namespace GitHub.Runner.Common
|
||||
}
|
||||
|
||||
public Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList,
|
||||
CancellationToken cancellationToken)
|
||||
CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors)
|
||||
{
|
||||
if (_launchClient != null)
|
||||
{
|
||||
return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList,
|
||||
cancellationToken: cancellationToken);
|
||||
if (!displayHelpfulActionsDownloadErrors)
|
||||
{
|
||||
return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
return _launchClient.GetResolveActionsDownloadInfoAsyncV2(planId, jobId, actionReferenceList, cancellationToken);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Launch client is not initialized.");
|
||||
|
||||
@@ -94,7 +94,9 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
CheckConnection();
|
||||
return RetryRequest<RenewJobResponse>(
|
||||
async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken);
|
||||
async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken,
|
||||
shouldRetry: ex =>
|
||||
ex is not TaskOrchestrationJobNotFoundException); // HTTP status 404
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,22 @@ namespace GitHub.Runner.Common.Util
|
||||
}
|
||||
return _defaultNodeVersion;
|
||||
}
|
||||
|
||||
/// <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, "node24", StringComparison.OrdinalIgnoreCase) &&
|
||||
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
|
||||
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
|
||||
{
|
||||
return ("node20", "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
|
||||
}
|
||||
|
||||
return (preferredVersion, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,19 @@ namespace GitHub.Runner.Listener
|
||||
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)
|
||||
{
|
||||
@@ -53,9 +66,22 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
Trace.Entering();
|
||||
|
||||
// Settings
|
||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||
_settings = configManager.LoadSettings();
|
||||
// Load settings if not provided through constructor
|
||||
if (_settings == null)
|
||||
{
|
||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||
_settings = configManager.LoadSettings();
|
||||
Trace.Info("Settings loaded from config manager");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info("Using provided settings");
|
||||
if (_isMigratedSettings)
|
||||
{
|
||||
Trace.Info("Using migrated settings from .runner_migrated");
|
||||
}
|
||||
}
|
||||
|
||||
var serverUrlV2 = _settings.ServerUrlV2;
|
||||
var serverUrl = _settings.ServerUrl;
|
||||
Trace.Info(_settings);
|
||||
@@ -141,7 +167,22 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Error("Catch exception during create session.");
|
||||
Trace.Error(ex);
|
||||
|
||||
if (ex is VssOAuthTokenRequestException vssOAuthEx && _credsV2.Federated is VssOAuthCredential vssOAuthCred)
|
||||
// If using migrated settings, limit the number of retries before returning failure
|
||||
if (_isMigratedSettings)
|
||||
{
|
||||
_migratedSettingsRetryCount++;
|
||||
Trace.Warning($"Migrated settings retry {_migratedSettingsRetryCount} of {_maxMigratedSettingsRetries}");
|
||||
|
||||
if (_migratedSettingsRetryCount >= _maxMigratedSettingsRetries)
|
||||
{
|
||||
Trace.Warning("Reached maximum retry attempts for migrated settings. Returning failure to try default settings.");
|
||||
return CreateSessionResult.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
if (!HostContext.AllowAuthMigration &&
|
||||
ex is VssOAuthTokenRequestException vssOAuthEx &&
|
||||
_credsV2.Federated is VssOAuthCredential vssOAuthCred)
|
||||
{
|
||||
// "invalid_client" means the runner registration has been deleted from the server.
|
||||
if (string.Equals(vssOAuthEx.Error, "invalid_client", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -162,7 +203,8 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsSessionCreationExceptionRetriable(ex))
|
||||
if (!HostContext.AllowAuthMigration &&
|
||||
!IsSessionCreationExceptionRetriable(ex))
|
||||
{
|
||||
_term.WriteError($"Failed to create session. {ex.Message}");
|
||||
if (ex is TaskAgentSessionConflictException)
|
||||
@@ -283,11 +325,11 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Info("Hosted runner has been deprovisioned.");
|
||||
throw;
|
||||
}
|
||||
catch (AccessDeniedException e) when (e.ErrorCode == 1)
|
||||
catch (AccessDeniedException e) when (e.ErrorCode == 1 && !HostContext.AllowAuthMigration)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (RunnerNotFoundException)
|
||||
catch (RunnerNotFoundException) when (!HostContext.AllowAuthMigration)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
@@ -296,7 +338,8 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Error("Catch exception during get next message.");
|
||||
Trace.Error(ex);
|
||||
|
||||
if (!IsGetNextMessageExceptionRetriable(ex))
|
||||
if (!HostContext.AllowAuthMigration &&
|
||||
!IsGetNextMessageExceptionRetriable(ex))
|
||||
{
|
||||
throw new NonRetryableException("Get next message failed with non-retryable error.", ex);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
Task UnconfigureAsync(CommandSettings command);
|
||||
void DeleteLocalRunnerConfig();
|
||||
RunnerSettings LoadSettings();
|
||||
RunnerSettings LoadMigratedSettings();
|
||||
}
|
||||
|
||||
public sealed class ConfigurationManager : RunnerService, IConfigurationManager
|
||||
@@ -66,6 +67,22 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
return settings;
|
||||
}
|
||||
|
||||
public RunnerSettings LoadMigratedSettings()
|
||||
{
|
||||
Trace.Info(nameof(LoadMigratedSettings));
|
||||
|
||||
// Check if migrated settings file exists
|
||||
if (!_store.IsMigratedConfigured())
|
||||
{
|
||||
throw new NonRetryableException("No migrated configuration found.");
|
||||
}
|
||||
|
||||
RunnerSettings settings = _store.GetMigratedSettings();
|
||||
Trace.Info("Migrated Settings Loaded");
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public async Task ConfigureAsync(CommandSettings command)
|
||||
{
|
||||
_term.WriteLine();
|
||||
@@ -370,6 +387,14 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
},
|
||||
};
|
||||
|
||||
if (agent.Properties.GetValue("EnableAuthMigrationByDefault", false) &&
|
||||
agent.Properties.TryGetValue<string>("AuthorizationUrlV2", out var authUrlV2) &&
|
||||
!string.IsNullOrEmpty(authUrlV2))
|
||||
{
|
||||
credentialData.Data["enableAuthMigrationByDefault"] = "true";
|
||||
credentialData.Data["authorizationUrlV2"] = authUrlV2;
|
||||
}
|
||||
|
||||
// Save the negotiated OAuth credential data
|
||||
_store.SaveCredential(credentialData);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,12 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
var jwt = JsonWebToken.Create(accessToken);
|
||||
var claims = jwt.ExtractClaims();
|
||||
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
|
||||
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orch_id", StringComparison.OrdinalIgnoreCase))?.Value;
|
||||
if (string.IsNullOrEmpty(orchestrationId))
|
||||
{
|
||||
orchestrationId = claims.FirstOrDefault(x => string.Equals(x.Type, "orchid", StringComparison.OrdinalIgnoreCase))?.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(orchestrationId))
|
||||
{
|
||||
Trace.Info($"Pull OrchestrationId {orchestrationId} from JWT claims");
|
||||
|
||||
@@ -315,11 +315,11 @@ namespace GitHub.Runner.Listener
|
||||
Trace.Info("Hosted runner has been deprovisioned.");
|
||||
throw;
|
||||
}
|
||||
catch (AccessDeniedException e) when (e.ErrorCode == 1)
|
||||
catch (AccessDeniedException e) when (e.ErrorCode == 1 && !HostContext.AllowAuthMigration)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (RunnerNotFoundException)
|
||||
catch (RunnerNotFoundException) when (!HostContext.AllowAuthMigration)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
@@ -333,11 +333,14 @@ namespace GitHub.Runner.Listener
|
||||
message = null;
|
||||
|
||||
// don't retry if SkipSessionRecover = true, DT service will delete agent session to stop agent from taking more jobs.
|
||||
if (ex is TaskAgentSessionExpiredException && !_settings.SkipSessionRecover && (await CreateSessionAsync(token) == CreateSessionResult.Success))
|
||||
if (!HostContext.AllowAuthMigration &&
|
||||
ex is TaskAgentSessionExpiredException &&
|
||||
!_settings.SkipSessionRecover && (await CreateSessionAsync(token) == CreateSessionResult.Success))
|
||||
{
|
||||
Trace.Info($"{nameof(TaskAgentSessionExpiredException)} received, recovered by recreate session.");
|
||||
}
|
||||
else if (!IsGetNextMessageExceptionRetriable(ex))
|
||||
else if (!HostContext.AllowAuthMigration &&
|
||||
!IsGetNextMessageExceptionRetriable(ex))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -15,7 +16,9 @@ using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Listener.Check;
|
||||
using GitHub.Runner.Listener.Configuration;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.OAuth;
|
||||
using GitHub.Services.WebApi;
|
||||
using GitHub.Services.WebApi.Jwt;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Listener
|
||||
@@ -35,10 +38,11 @@ namespace GitHub.Runner.Listener
|
||||
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 bool _runnerExiting = false;
|
||||
private bool _hasTerminationGracePeriod = false;
|
||||
private CancellationTokenSource _authMigrationClaimsCheckTokenSource = new();
|
||||
|
||||
// <summary>
|
||||
// Helps avoid excessive calls to Run Service when encountering non-retriable errors from /acquirejob.
|
||||
@@ -311,12 +315,6 @@ 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);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Constants.Variables.Agent.ActionsTerminationGracePeriodSeconds)))
|
||||
{
|
||||
_hasTerminationGracePeriod = true;
|
||||
Trace.Verbose($"Runner has termination grace period set");
|
||||
}
|
||||
|
||||
var cred = store.GetCredentials();
|
||||
if (cred != null &&
|
||||
cred.Scheme == Constants.Configuration.OAuth &&
|
||||
@@ -327,7 +325,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
|
||||
// Run the runner interactively or as service
|
||||
return await RunAsync(settings, command.RunOnce || settings.Ephemeral);
|
||||
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -337,6 +335,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
finally
|
||||
{
|
||||
_authMigrationClaimsCheckTokenSource?.Cancel();
|
||||
_authMigrationTelemetryTokenSource?.Cancel();
|
||||
HostContext.AuthMigrationChanged -= HandleAuthMigrationChanged;
|
||||
_term.CancelKeyPress -= CtrlCHandler;
|
||||
@@ -347,10 +346,9 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
private void Runner_Unloading(object sender, EventArgs e)
|
||||
{
|
||||
_runnerExiting = true;
|
||||
if ((!_inConfigStage) && (!HostContext.RunnerShutdownToken.IsCancellationRequested))
|
||||
{
|
||||
HostContext.ShutdownRunner(ShutdownReason.UserCancelled, GetShutdownDelay());
|
||||
HostContext.ShutdownRunner(ShutdownReason.UserCancelled);
|
||||
_completedCommand.WaitOne(Constants.Runner.ExitOnUnloadTimeout);
|
||||
}
|
||||
}
|
||||
@@ -358,7 +356,6 @@ namespace GitHub.Runner.Listener
|
||||
private void CtrlCHandler(object sender, EventArgs e)
|
||||
{
|
||||
_term.WriteLine("Exiting...");
|
||||
_runnerExiting = true;
|
||||
if (_inConfigStage)
|
||||
{
|
||||
HostContext.Dispose();
|
||||
@@ -381,33 +378,21 @@ namespace GitHub.Runner.Listener
|
||||
reason = ShutdownReason.UserCancelled;
|
||||
}
|
||||
|
||||
HostContext.ShutdownRunner(reason, GetShutdownDelay());
|
||||
HostContext.ShutdownRunner(reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
HostContext.ShutdownRunner(ShutdownReason.UserCancelled, GetShutdownDelay());
|
||||
HostContext.ShutdownRunner(ShutdownReason.UserCancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleJobStatusEvent(object sender, JobStatusEventArgs e)
|
||||
{
|
||||
if (_hasTerminationGracePeriod &&
|
||||
e != null &&
|
||||
e.Status != TaskAgentStatus.Busy &&
|
||||
_runnerExiting)
|
||||
{
|
||||
Trace.Info("Runner is no longer busy, shutting down.");
|
||||
HostContext.ShutdownRunner(ShutdownReason.UserCancelled);
|
||||
}
|
||||
}
|
||||
|
||||
private IMessageListener GetMessageListener(RunnerSettings settings)
|
||||
private IMessageListener GetMessageListener(RunnerSettings settings, bool isMigratedSettings = false)
|
||||
{
|
||||
if (settings.UseV2Flow)
|
||||
{
|
||||
Trace.Info($"Using BrokerMessageListener");
|
||||
var brokerListener = new BrokerMessageListener();
|
||||
var brokerListener = new BrokerMessageListener(settings, isMigratedSettings);
|
||||
brokerListener.Initialize(HostContext);
|
||||
return brokerListener;
|
||||
}
|
||||
@@ -421,15 +406,65 @@ namespace GitHub.Runner.Listener
|
||||
try
|
||||
{
|
||||
Trace.Info(nameof(RunAsync));
|
||||
_listener = GetMessageListener(settings);
|
||||
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
||||
if (createSessionResult == CreateSessionResult.SessionConflict)
|
||||
|
||||
// First try using migrated settings if available
|
||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||
RunnerSettings migratedSettings = null;
|
||||
|
||||
try
|
||||
{
|
||||
return Constants.Runner.ReturnCode.SessionConflict;
|
||||
migratedSettings = configManager.LoadMigratedSettings();
|
||||
Trace.Info("Loaded migrated settings from .runner_migrated file");
|
||||
Trace.Info(migratedSettings);
|
||||
}
|
||||
else if (createSessionResult == CreateSessionResult.Failure)
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Constants.Runner.ReturnCode.TerminatedError;
|
||||
// If migrated settings file doesn't exist or can't be loaded, we'll use the provided settings
|
||||
Trace.Info($"Failed to load migrated settings: {ex.Message}");
|
||||
}
|
||||
|
||||
bool usedMigratedSettings = false;
|
||||
|
||||
if (migratedSettings != null)
|
||||
{
|
||||
// Try to create session with migrated settings first
|
||||
Trace.Info("Attempting to create session using migrated settings");
|
||||
_listener = GetMessageListener(migratedSettings, isMigratedSettings: true);
|
||||
|
||||
try
|
||||
{
|
||||
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
||||
if (createSessionResult == CreateSessionResult.Success)
|
||||
{
|
||||
Trace.Info("Successfully created session with migrated settings");
|
||||
settings = migratedSettings; // Use migrated settings for the rest of the process
|
||||
usedMigratedSettings = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Warning($"Failed to create session with migrated settings: {createSessionResult}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error($"Exception when creating session with migrated settings: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// If migrated settings weren't used or session creation failed, use original settings
|
||||
if (!usedMigratedSettings)
|
||||
{
|
||||
Trace.Info("Falling back to original .runner settings");
|
||||
_listener = GetMessageListener(settings);
|
||||
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
||||
if (createSessionResult == CreateSessionResult.SessionConflict)
|
||||
{
|
||||
return Constants.Runner.ReturnCode.SessionConflict;
|
||||
}
|
||||
else if (createSessionResult == CreateSessionResult.Failure)
|
||||
{
|
||||
return Constants.Runner.ReturnCode.TerminatedError;
|
||||
}
|
||||
}
|
||||
|
||||
HostContext.WritePerfCounter("SessionCreated");
|
||||
@@ -443,6 +478,8 @@ namespace GitHub.Runner.Listener
|
||||
// Should we try to cleanup ephemeral runners
|
||||
bool runOnceJobCompleted = false;
|
||||
bool skipSessionDeletion = false;
|
||||
bool restartSession = false; // Flag to indicate session restart
|
||||
bool restartSessionPending = false;
|
||||
try
|
||||
{
|
||||
var notification = HostContext.GetService<IJobNotification>();
|
||||
@@ -452,16 +489,21 @@ namespace GitHub.Runner.Listener
|
||||
bool autoUpdateInProgress = false;
|
||||
Task<bool> selfUpdateTask = null;
|
||||
bool runOnceJobReceived = false;
|
||||
jobDispatcher = HostContext.GetService<IJobDispatcher>();
|
||||
jobDispatcher = HostContext.CreateService<IJobDispatcher>();
|
||||
|
||||
jobDispatcher.JobStatus += _listener.OnJobStatus;
|
||||
if (_hasTerminationGracePeriod)
|
||||
{
|
||||
jobDispatcher.JobStatus += HandleJobStatusEvent;
|
||||
}
|
||||
|
||||
while (!HostContext.RunnerShutdownToken.IsCancellationRequested)
|
||||
{
|
||||
// Check if we need to restart the session and can do so (job dispatcher not busy)
|
||||
if (restartSessionPending && !jobDispatcher.Busy)
|
||||
{
|
||||
Trace.Info("Pending session restart detected and job dispatcher is not busy. Restarting session now.");
|
||||
messageQueueLoopTokenSource.Cancel();
|
||||
restartSession = true;
|
||||
break;
|
||||
}
|
||||
|
||||
TaskAgentMessage message = null;
|
||||
bool skipMessageDeletion = false;
|
||||
try
|
||||
@@ -698,6 +740,17 @@ namespace GitHub.Runner.Listener
|
||||
configType: runnerRefreshConfigMessage.ConfigType,
|
||||
serviceType: runnerRefreshConfigMessage.ServiceType,
|
||||
configRefreshUrl: runnerRefreshConfigMessage.ConfigRefreshUrl);
|
||||
|
||||
// Set flag to schedule session restart if ConfigType is "runner"
|
||||
if (string.Equals(runnerRefreshConfigMessage.ConfigType, "runner", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Trace.Info("Runner configuration was updated. Session restart has been scheduled");
|
||||
restartSessionPending = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info($"No session restart needed for config type: {runnerRefreshConfigMessage.ConfigType}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -729,10 +782,6 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
if (jobDispatcher != null)
|
||||
{
|
||||
if (_hasTerminationGracePeriod)
|
||||
{
|
||||
jobDispatcher.JobStatus -= HandleJobStatusEvent;
|
||||
}
|
||||
jobDispatcher.JobStatus -= _listener.OnJobStatus;
|
||||
await jobDispatcher.ShutdownAsync();
|
||||
}
|
||||
@@ -756,10 +805,16 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
if (settings.Ephemeral && runOnceJobCompleted)
|
||||
{
|
||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||
configManager.DeleteLocalRunnerConfig();
|
||||
}
|
||||
}
|
||||
|
||||
// After cleanup, check if we need to restart the session
|
||||
if (restartSession)
|
||||
{
|
||||
Trace.Info("Restarting runner session after config update...");
|
||||
return Constants.Runner.ReturnCode.RunnerConfigurationRefreshed;
|
||||
}
|
||||
}
|
||||
catch (TaskAgentAccessTokenExpiredException)
|
||||
{
|
||||
@@ -773,6 +828,28 @@ namespace GitHub.Runner.Listener
|
||||
return Constants.Runner.ReturnCode.Success;
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce)
|
||||
{
|
||||
int returnCode = Constants.Runner.ReturnCode.Success;
|
||||
bool restart = false;
|
||||
do
|
||||
{
|
||||
restart = false;
|
||||
returnCode = await RunAsync(settings, runOnce);
|
||||
|
||||
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
|
||||
{
|
||||
Trace.Info("Runner configuration was refreshed, restarting session...");
|
||||
// Reload settings in case they changed
|
||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||
settings = configManager.LoadSettings();
|
||||
restart = true;
|
||||
}
|
||||
} while (restart);
|
||||
|
||||
return returnCode;
|
||||
}
|
||||
|
||||
private void HandleAuthMigrationChanged(object sender, AuthMigrationEventArgs e)
|
||||
{
|
||||
Trace.Verbose("Handle AuthMigrationChanged in Runner");
|
||||
@@ -786,6 +863,131 @@ namespace GitHub.Runner.Listener
|
||||
_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)
|
||||
@@ -840,34 +1042,6 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
private TimeSpan GetShutdownDelay()
|
||||
{
|
||||
TimeSpan delay = TimeSpan.Zero;
|
||||
if (_hasTerminationGracePeriod)
|
||||
{
|
||||
var jobDispatcher = HostContext.GetService<IJobDispatcher>();
|
||||
if (jobDispatcher.Busy)
|
||||
{
|
||||
Trace.Info("Runner is busy, checking for grace period.");
|
||||
var delayEnv = Environment.GetEnvironmentVariable(Constants.Variables.Agent.ActionsTerminationGracePeriodSeconds);
|
||||
if (!string.IsNullOrEmpty(delayEnv) &&
|
||||
int.TryParse(delayEnv, out int delaySeconds) &&
|
||||
delaySeconds > 0 &&
|
||||
delaySeconds < 60 * 60) // 1 hour
|
||||
{
|
||||
Trace.Info($"Waiting for {delaySeconds} seconds before shutting down.");
|
||||
delay = TimeSpan.FromSeconds(delaySeconds);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Verbose("Runner is not busy, no grace period.");
|
||||
}
|
||||
}
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
private void PrintUsage(CommandSettings command)
|
||||
{
|
||||
string separator;
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace GitHub.Runner.Sdk
|
||||
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY")))
|
||||
{
|
||||
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||
RawClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||
}
|
||||
|
||||
var rawHeaderValues = new List<ProductInfoHeaderValue>();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7" />
|
||||
</startup>
|
||||
</configuration>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(PackageRuntime)' != 'win-arm64' ">
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
|
||||
@@ -688,7 +688,8 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
|
||||
{
|
||||
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
|
||||
var displayHelpfulActionsDownloadErrors = executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.DisplayHelpfulActionsDownloadErrors) ?? false;
|
||||
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -450,7 +450,8 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase))
|
||||
string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrEmpty(mainToken?.Value))
|
||||
{
|
||||
@@ -490,7 +491,7 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16' or 'node20' instead.");
|
||||
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
|
||||
}
|
||||
}
|
||||
else if (pluginToken != null)
|
||||
@@ -501,7 +502,7 @@ namespace GitHub.Runner.Worker
|
||||
};
|
||||
}
|
||||
|
||||
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'.");
|
||||
throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.");
|
||||
}
|
||||
|
||||
private void ConvertInputs(
|
||||
|
||||
@@ -862,7 +862,21 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
ExpressionValues["secrets"] = Global.Variables.ToSecretsContext();
|
||||
ExpressionValues["runner"] = new RunnerContext();
|
||||
ExpressionValues["job"] = new JobContext();
|
||||
|
||||
Trace.Info("Initializing Job context");
|
||||
var jobContext = new JobContext();
|
||||
if (Global.Variables.GetBoolean(Constants.Runner.Features.AddCheckRunIdToJobContext) ?? false)
|
||||
{
|
||||
ExpressionValues.TryGetValue("job", out var jobDictionary);
|
||||
if (jobDictionary != null)
|
||||
{
|
||||
foreach (var pair in jobDictionary.AssertDictionary("job"))
|
||||
{
|
||||
jobContext[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
ExpressionValues["job"] = jobContext;
|
||||
|
||||
Trace.Info("Initialize GitHub context");
|
||||
var githubAccessToken = new StringContextData(Global.Variables.Get("system.github.token"));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -9,7 +8,6 @@ using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Linq;
|
||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||
using System.IO;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace GitHub.Runner.Worker.Handlers
|
||||
@@ -60,7 +58,14 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
||||
{
|
||||
return Task.FromResult<string>(preferredVersion);
|
||||
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
|
||||
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
|
||||
if (!string.IsNullOrEmpty(warningMessage))
|
||||
{
|
||||
executionContext.Warning(warningMessage);
|
||||
}
|
||||
|
||||
return Task.FromResult(nodeVersion);
|
||||
}
|
||||
|
||||
public async Task<int> ExecuteAsync(IExecutionContext context,
|
||||
@@ -137,8 +142,12 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
||||
{
|
||||
// Optimistically use the default
|
||||
string nodeExternal = preferredVersion;
|
||||
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
|
||||
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
|
||||
if (!string.IsNullOrEmpty(warningMessage))
|
||||
{
|
||||
executionContext.Warning(warningMessage);
|
||||
}
|
||||
|
||||
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
||||
{
|
||||
@@ -264,7 +273,14 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
|
||||
{
|
||||
string nodeExternal = preferredVersion;
|
||||
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
|
||||
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
|
||||
if (!string.IsNullOrEmpty(warningMessage))
|
||||
{
|
||||
executionContext.Warning(warningMessage);
|
||||
}
|
||||
|
||||
// Check for Alpine container compatibility
|
||||
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
|
||||
{
|
||||
var os = Constants.Runner.Platform.ToString();
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace GitHub.Runner.Worker
|
||||
public sealed class IssueMatcher
|
||||
{
|
||||
private string _defaultSeverity;
|
||||
private string _defaultFromPath;
|
||||
private string _owner;
|
||||
private IssuePattern[] _patterns;
|
||||
private IssueMatch[] _state;
|
||||
@@ -29,6 +30,7 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
_owner = config.Owner;
|
||||
_defaultSeverity = config.Severity;
|
||||
_defaultFromPath = config.FromPath;
|
||||
_patterns = config.Patterns.Select(x => new IssuePattern(x, timeout)).ToArray();
|
||||
Reset();
|
||||
}
|
||||
@@ -59,6 +61,19 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
public string DefaultFromPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_defaultFromPath == null)
|
||||
{
|
||||
_defaultFromPath = string.Empty;
|
||||
}
|
||||
|
||||
return _defaultFromPath;
|
||||
}
|
||||
}
|
||||
|
||||
public IssueMatch Match(string line)
|
||||
{
|
||||
// Single pattern
|
||||
@@ -69,7 +84,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
if (regexMatch.Success)
|
||||
{
|
||||
return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity);
|
||||
return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -110,7 +125,7 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
// Return
|
||||
return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity);
|
||||
return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
|
||||
}
|
||||
// Not the last pattern
|
||||
else
|
||||
@@ -184,7 +199,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public sealed class IssueMatch
|
||||
{
|
||||
public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null)
|
||||
public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null, string defaultFromPath = null)
|
||||
{
|
||||
File = runningMatch?.File ?? GetValue(groups, pattern.File);
|
||||
Line = runningMatch?.Line ?? GetValue(groups, pattern.Line);
|
||||
@@ -198,6 +213,11 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
Severity = defaultSeverity;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(FromPath) && !string.IsNullOrEmpty(defaultFromPath))
|
||||
{
|
||||
FromPath = defaultFromPath;
|
||||
}
|
||||
}
|
||||
|
||||
public string File { get; }
|
||||
@@ -282,6 +302,9 @@ namespace GitHub.Runner.Worker
|
||||
[DataMember(Name = "pattern")]
|
||||
private IssuePatternConfig[] _patterns;
|
||||
|
||||
[DataMember(Name = "fromPath")]
|
||||
private string _fromPath;
|
||||
|
||||
public string Owner
|
||||
{
|
||||
get
|
||||
@@ -318,6 +341,24 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
public string FromPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_fromPath == null)
|
||||
{
|
||||
_fromPath = string.Empty;
|
||||
}
|
||||
|
||||
return _fromPath;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_fromPath = value;
|
||||
}
|
||||
}
|
||||
|
||||
public IssuePatternConfig[] Patterns
|
||||
{
|
||||
get
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
@@ -56,5 +56,31 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double? CheckRunId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.TryGetValue("check_run_id", out var value) && value is NumberContextData number)
|
||||
{
|
||||
return number.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
this["check_run_id"] = new NumberContextData(value.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
this["check_run_id"] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,11 @@ namespace GitHub.Runner.Worker
|
||||
if (message.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out VariableValue orchestrationId) &&
|
||||
!string.IsNullOrEmpty(orchestrationId.Value))
|
||||
{
|
||||
// make the orchestration id the first item in the user-agent header to avoid get truncated in server log.
|
||||
HostContext.UserAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", orchestrationId.Value));
|
||||
if (!HostContext.UserAgents.Any(x => string.Equals(x.Product?.Name, "OrchestrationId", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// make the orchestration id the first item in the user-agent header to avoid get truncated in server log.
|
||||
HostContext.UserAgents.Insert(0, new ProductInfoHeaderValue("OrchestrationId", orchestrationId.Value));
|
||||
}
|
||||
|
||||
// make sure orchestration id is in the user-agent header.
|
||||
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -106,6 +106,18 @@ namespace GitHub.Services.Common
|
||||
{
|
||||
VssTraceActivity traceActivity = VssTraceActivity.Current;
|
||||
|
||||
if (!m_appliedServerCertificateValidationCallbackToTransportHandler &&
|
||||
request.RequestUri.Scheme == "https")
|
||||
{
|
||||
HttpClientHandler httpClientHandler = m_transportHandler as HttpClientHandler;
|
||||
if (httpClientHandler != null &&
|
||||
this.Settings.ServerCertificateValidationCallback != null)
|
||||
{
|
||||
httpClientHandler.ServerCertificateCustomValidationCallback = this.Settings.ServerCertificateValidationCallback;
|
||||
}
|
||||
m_appliedServerCertificateValidationCallbackToTransportHandler = true;
|
||||
}
|
||||
|
||||
lock (m_thisLock)
|
||||
{
|
||||
// Ensure that we attempt to use the most appropriate authentication mechanism by default.
|
||||
@@ -291,6 +303,7 @@ namespace GitHub.Services.Common
|
||||
}
|
||||
}
|
||||
|
||||
private bool m_appliedServerCertificateValidationCallbackToTransportHandler;
|
||||
private readonly HttpMessageHandler m_transportHandler;
|
||||
private HttpMessageInvoker m_messageInvoker;
|
||||
private CredentialWrapper m_credentialWrapper;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.24.0" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace GitHub.Services.Launch.Contracts
|
||||
{
|
||||
[DataMember(EmitDefaultValue = false, Name = "authentication")]
|
||||
public ActionDownloadAuthenticationResponse Authentication { get; set; }
|
||||
|
||||
|
||||
[DataMember(EmitDefaultValue = false, Name = "package_details")]
|
||||
public ActionDownloadPackageDetailsResponse PackageDetails { get; set; }
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace GitHub.Services.Launch.Contracts
|
||||
|
||||
|
||||
[DataContract]
|
||||
public class ActionDownloadPackageDetailsResponse
|
||||
public class ActionDownloadPackageDetailsResponse
|
||||
{
|
||||
[DataMember(EmitDefaultValue = false, Name = "version")]
|
||||
public string Version { get; set; }
|
||||
@@ -81,4 +81,25 @@ namespace GitHub.Services.Launch.Contracts
|
||||
[DataMember(EmitDefaultValue = false, Name = "actions")]
|
||||
public IDictionary<string, ActionDownloadInfoResponse> Actions { get; set; }
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public class ActionDownloadResolutionError
|
||||
{
|
||||
/// <summary>
|
||||
/// The error message associated with the action download error.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false, Name = "message")]
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
public class ActionDownloadResolutionErrorCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// A mapping of action specifications to their download errors.
|
||||
/// <remarks>The key is the full name of the action plus version, e.g. "actions/checkout@v2".</remarks>
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false, Name = "errors")]
|
||||
public IDictionary<string, ActionDownloadResolutionError> Errors { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Formatting;
|
||||
using System.Net.Http.Headers;
|
||||
@@ -32,11 +33,52 @@ namespace GitHub.Services.Launch.Client
|
||||
public async Task<ActionDownloadInfoCollection> GetResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken)
|
||||
{
|
||||
var GetResolveActionsDownloadInfoURLEndpoint = new Uri(m_launchServiceUrl, $"/actions/build/{planId.ToString()}/jobs/{jobId.ToString()}/runnerresolve/actions");
|
||||
return ToServerData(await GetLaunchSignedURLResponse<ActionReferenceRequestList, ActionDownloadInfoResponseCollection>(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken));
|
||||
var response = await GetLaunchSignedURLResponse<ActionReferenceRequestList>(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken);
|
||||
return ToServerData(await ReadJsonContentAsync<ActionDownloadInfoResponseCollection>(response, cancellationToken));
|
||||
}
|
||||
|
||||
// Resolve Actions
|
||||
private async Task<T> GetLaunchSignedURLResponse<R, T>(Uri uri, R request, CancellationToken cancellationToken)
|
||||
public async Task<ActionDownloadInfoCollection> GetResolveActionsDownloadInfoAsyncV2(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken)
|
||||
{
|
||||
var GetResolveActionsDownloadInfoURLEndpoint = new Uri(m_launchServiceUrl, $"/actions/build/{planId.ToString()}/jobs/{jobId.ToString()}/runnerresolve/actions");
|
||||
var response = await GetLaunchSignedURLResponse<ActionReferenceRequestList>(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// Success response - deserialize the action download info
|
||||
return ToServerData(await ReadJsonContentAsync<ActionDownloadInfoResponseCollection>(response, cancellationToken));
|
||||
}
|
||||
|
||||
var responseError = response.ReasonPhrase ?? "";
|
||||
if (response.StatusCode == HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
// 422 response - unresolvable actions, error details are in the body
|
||||
var errors = await ReadJsonContentAsync<ActionDownloadResolutionErrorCollection>(response, cancellationToken);
|
||||
string combinedErrorMessage;
|
||||
if (errors?.Errors != null && errors.Errors.Any())
|
||||
{
|
||||
combinedErrorMessage = String.Join(". ", errors.Errors.Select(kvp => kvp.Value.Message));
|
||||
}
|
||||
else
|
||||
{
|
||||
combinedErrorMessage = responseError;
|
||||
}
|
||||
|
||||
throw new UnresolvableActionDownloadInfoException(combinedErrorMessage);
|
||||
}
|
||||
else if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
// Here we want to add a message so customers don't think it's a rate limit scoped to them
|
||||
// Ideally this would be 500 but the runner retries 500s, which we don't want to do when we're being rate limited
|
||||
// See: https://github.com/github/ecosystem-api/issues/4084
|
||||
throw new NonRetryableActionDownloadInfoException(responseError + " (GitHub has reached an internal rate limit, please try again later)");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(responseError);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> GetLaunchSignedURLResponse<R>(Uri uri, R request, CancellationToken cancellationToken)
|
||||
{
|
||||
using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri))
|
||||
{
|
||||
@@ -46,10 +88,7 @@ namespace GitHub.Services.Launch.Client
|
||||
using (HttpContent content = new ObjectContent<R>(request, m_formatter))
|
||||
{
|
||||
requestMessage.Content = content;
|
||||
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
|
||||
{
|
||||
return await ReadJsonContentAsync<T>(response, cancellationToken);
|
||||
}
|
||||
return await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,8 +520,8 @@ namespace GitHub.Services.Results.Client
|
||||
Number = r.Order.GetValueOrDefault(),
|
||||
Name = r.Name,
|
||||
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
|
||||
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat),
|
||||
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat),
|
||||
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture),
|
||||
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture),
|
||||
Conclusion = ConvertResultToConclusion(r.Result)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -363,6 +363,49 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
public async Task CreatesSessionWithProvidedSettings()
|
||||
{
|
||||
using (TestHostContext tc = CreateTestContext())
|
||||
using (var tokenSource = new CancellationTokenSource())
|
||||
{
|
||||
Tracing trace = tc.GetTrace();
|
||||
|
||||
// Arrange.
|
||||
var expectedSession = new TaskAgentSession();
|
||||
_brokerServer
|
||||
.Setup(x => x.CreateSessionAsync(
|
||||
It.Is<TaskAgentSession>(y => y != null),
|
||||
tokenSource.Token))
|
||||
.Returns(Task.FromResult(expectedSession));
|
||||
|
||||
_credMgr.Setup(x => x.LoadCredentials(true)).Returns(new VssCredentials());
|
||||
|
||||
// Make sure the config is never called when settings are provided
|
||||
_config.Setup(x => x.LoadSettings()).Throws(new InvalidOperationException("Should not be called"));
|
||||
|
||||
// Act.
|
||||
// Use the constructor that accepts settings
|
||||
BrokerMessageListener listener = new(_settings);
|
||||
listener.Initialize(tc);
|
||||
|
||||
CreateSessionResult result = await listener.CreateSessionAsync(tokenSource.Token);
|
||||
trace.Info("result: {0}", result);
|
||||
|
||||
// Assert.
|
||||
Assert.Equal(CreateSessionResult.Success, result);
|
||||
_brokerServer
|
||||
.Verify(x => x.CreateSessionAsync(
|
||||
It.Is<TaskAgentSession>(y => y != null),
|
||||
tokenSource.Token), Times.Once());
|
||||
|
||||
// Verify LoadSettings was never called
|
||||
_config.Verify(x => x.LoadSettings(), Times.Never());
|
||||
}
|
||||
}
|
||||
|
||||
private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
|
||||
{
|
||||
TestHostContext tc = new(this, testName);
|
||||
|
||||
@@ -126,7 +126,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
|
||||
});
|
||||
|
||||
hc.SetSingleton<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||
|
||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||
//Act
|
||||
@@ -309,7 +309,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
|
||||
});
|
||||
|
||||
hc.SetSingleton<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||
|
||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||
//Act
|
||||
@@ -413,7 +413,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
|
||||
});
|
||||
|
||||
hc.SetSingleton<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||
|
||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||
//Act
|
||||
@@ -503,7 +503,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
|
||||
});
|
||||
|
||||
hc.SetSingleton<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||
|
||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||
//Act
|
||||
@@ -578,7 +578,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
hc.SetSingleton<IConfigurationStore>(_configStore.Object);
|
||||
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
|
||||
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||
hc.SetSingleton<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||
|
||||
runner.Initialize(hc);
|
||||
var settings = new RunnerSettings
|
||||
@@ -679,7 +679,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
|
||||
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||
hc.EnqueueInstance<IActionsRunServer>(_actionsRunServer.Object);
|
||||
hc.SetSingleton<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||
|
||||
runner.Initialize(hc);
|
||||
var settings = new RunnerSettings
|
||||
@@ -780,7 +780,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
|
||||
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||
hc.EnqueueInstance<IRunServer>(_runServer.Object);
|
||||
hc.SetSingleton<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||
|
||||
runner.Initialize(hc);
|
||||
var settings = new RunnerSettings
|
||||
@@ -880,7 +880,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
||||
hc.SetSingleton<ISelfUpdater>(_updater.Object);
|
||||
hc.SetSingleton<ICredentialManager>(_credentialManager.Object);
|
||||
hc.EnqueueInstance<IErrorThrottler>(_acquireJobThrottler.Object);
|
||||
hc.SetSingleton<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IJobDispatcher>(_jobDispatcher.Object);
|
||||
hc.EnqueueInstance<IRunServer>(_runServer.Object);
|
||||
hc.EnqueueInstance<IRunServer>(_runServer.Object);
|
||||
|
||||
|
||||
388
src/Test/L0/Listener/ShellScriptSyntaxL0.cs
Normal file
388
src/Test/L0/Listener/ShellScriptSyntaxL0.cs
Normal file
@@ -0,0 +1,388 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using GitHub.Runner.Common.Tests;
|
||||
using GitHub.Runner.Sdk;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Listener
|
||||
{
|
||||
public sealed class ShellScriptSyntaxL0
|
||||
{
|
||||
// Generic method to test any shell script template for bash syntax errors
|
||||
private void ValidateShellScriptTemplateSyntax(string relativePath, string templateName, bool shouldPass = true, Func<string, string> templateModifier = null)
|
||||
{
|
||||
// Skip on Windows
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var hc = new TestHostContext(this))
|
||||
{
|
||||
// Arrange
|
||||
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
||||
string templatePath = Path.Combine(rootDirectory, relativePath, templateName);
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
string tempScriptPath = Path.Combine(tempDir, Path.GetFileNameWithoutExtension(templateName));
|
||||
|
||||
// Read the template
|
||||
string template = File.ReadAllText(templatePath);
|
||||
|
||||
// Apply template modifier if provided (for injecting errors)
|
||||
if (templateModifier != null)
|
||||
{
|
||||
template = templateModifier(template);
|
||||
}
|
||||
|
||||
// Replace common placeholders with valid test values
|
||||
template = ReplaceCommonPlaceholders(template, rootDirectory, tempDir);
|
||||
|
||||
// Write the processed template to a temporary file
|
||||
File.WriteAllText(tempScriptPath, template);
|
||||
|
||||
// Make the file executable
|
||||
var chmodProcess = new Process();
|
||||
chmodProcess.StartInfo.FileName = "chmod";
|
||||
chmodProcess.StartInfo.Arguments = $"+x {tempScriptPath}";
|
||||
chmodProcess.Start();
|
||||
chmodProcess.WaitForExit();
|
||||
|
||||
// Act - Check syntax using bash -n
|
||||
var process = new Process();
|
||||
process.StartInfo.FileName = "bash";
|
||||
process.StartInfo.Arguments = $"-n {tempScriptPath}";
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.Start();
|
||||
string errors = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
// Assert based on expected outcome
|
||||
if (shouldPass)
|
||||
{
|
||||
Assert.Equal(0, process.ExitCode);
|
||||
Assert.Empty(errors);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.NotEqual(0, process.ExitCode);
|
||||
Assert.NotEmpty(errors);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
try
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Fail($"Exception during test for {templateName}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to replace common placeholders in shell script templates
|
||||
private string ReplaceCommonPlaceholders(string template, string rootDirectory, string tempDir)
|
||||
{
|
||||
// Replace common placeholders
|
||||
template = template.Replace("_PROCESS_ID_", "1234");
|
||||
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener");
|
||||
template = template.Replace("_ROOT_FOLDER_", rootDirectory);
|
||||
template = template.Replace("_EXIST_RUNNER_VERSION_", "2.300.0");
|
||||
template = template.Replace("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
|
||||
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
|
||||
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
|
||||
template = template.Replace("_SERVICEUSERNAME_", "runner");
|
||||
template = template.Replace("_SERVICEPASSWORD_", "password");
|
||||
template = template.Replace("_SERVICEDISPLAYNAME_", "GitHub Actions Runner");
|
||||
template = template.Replace("_SERVICENAME_", "github-runner");
|
||||
template = template.Replace("_SERVICELOGPATH_", Path.Combine(tempDir, "service.log"));
|
||||
template = template.Replace("_RUNNERSERVICEUSERDISPLAYNAME_", "GitHub Actions Runner Service");
|
||||
|
||||
return template;
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
[Trait("SkipOn", "windows")]
|
||||
public void UpdateShTemplateHasValidSyntax()
|
||||
{
|
||||
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "update.sh.template");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
[Trait("SkipOn", "windows")]
|
||||
public void UpdateShTemplateWithErrorsFailsValidation()
|
||||
{
|
||||
ValidateShellScriptTemplateSyntax(
|
||||
"src/Misc/layoutbin",
|
||||
"update.sh.template",
|
||||
shouldPass: false,
|
||||
templateModifier: template =>
|
||||
{
|
||||
// Introduce syntax errors
|
||||
|
||||
// 1. Missing 'fi' for an 'if' statement
|
||||
template = template.Replace("fi\n", "\n");
|
||||
|
||||
// 2. Unbalanced quotes
|
||||
template = template.Replace("date \"+[%F %T-%4N]", "date \"+[%F %T-%4N");
|
||||
|
||||
// 3. Invalid syntax in if condition
|
||||
template = template.Replace("if [ $? -ne 0 ]", "if [ $? -ne 0");
|
||||
|
||||
return template;
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
[Trait("SkipOn", "windows")]
|
||||
public void DarwinSvcShTemplateHasValidSyntax()
|
||||
{
|
||||
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "darwin.svc.sh.template");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
[Trait("SkipOn", "windows")]
|
||||
public void SystemdSvcShTemplateHasValidSyntax()
|
||||
{
|
||||
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "systemd.svc.sh.template");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
[Trait("SkipOn", "windows")]
|
||||
public void RunHelperShTemplateHasValidSyntax()
|
||||
{
|
||||
ValidateShellScriptTemplateSyntax("src/Misc/layoutroot", "run-helper.sh.template");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
[Trait("SkipOn", "windows")]
|
||||
public void UpdateShTemplateHasCorrectVariableReferencesAndIfStructure()
|
||||
{
|
||||
// Skip on Windows
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var hc = new TestHostContext(this))
|
||||
{
|
||||
// Arrange
|
||||
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
||||
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", "update.sh.template");
|
||||
|
||||
// Read the template
|
||||
string template = File.ReadAllText(templatePath);
|
||||
|
||||
// Assert
|
||||
// 1. Check that $restartinteractiverunner is correctly referenced with $ in if condition
|
||||
Assert.Contains("if [[ \"$currentplatform\" == 'darwin' && $restartinteractiverunner -eq 0 ]];\nthen", template);
|
||||
|
||||
// 2. Check for proper nesting of if statements for node version checks
|
||||
int nodeVersionCheckLines = 0;
|
||||
bool foundNode24Block = false;
|
||||
bool foundNode16Block = false;
|
||||
bool foundNode12Block = false;
|
||||
bool hasProperIndentation = false;
|
||||
|
||||
string[] lines = template.Split('\n');
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
string line = lines[i];
|
||||
if (line.Contains("nodever=\"node24\""))
|
||||
{
|
||||
foundNode24Block = true;
|
||||
}
|
||||
if (line.Contains("nodever=\"node16\""))
|
||||
{
|
||||
foundNode16Block = true;
|
||||
}
|
||||
if (foundNode16Block && line.Contains("nodever=\"node12\""))
|
||||
{
|
||||
foundNode12Block = true;
|
||||
// Check if we have proper indentation for this nested block
|
||||
hasProperIndentation = line.StartsWith(" ");
|
||||
}
|
||||
if (line.Contains("Fallback if RunnerService.js was started with"))
|
||||
{
|
||||
nodeVersionCheckLines++;
|
||||
}
|
||||
}
|
||||
|
||||
// The template has node24 check but there's no "Fallback if RunnerService.js was started with node24" comment for it
|
||||
// Only the node20, node16, and node12 sections have this comment
|
||||
Assert.Equal(3, nodeVersionCheckLines); // node20, node16, node12
|
||||
Assert.True(foundNode24Block, "Could not find node24 block");
|
||||
Assert.True(foundNode16Block, "Could not find node16 block");
|
||||
Assert.True(foundNode12Block, "Could not find node12 block");
|
||||
Assert.True(hasProperIndentation, "node12 block is not properly indented");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Fail($"Exception during test: {ex.ToString()}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
[Trait("SkipOn", "osx,linux")]
|
||||
public void UpdateCmdTemplateHasValidSyntax()
|
||||
{
|
||||
// Skip on non-Windows platforms
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Runner")]
|
||||
[Trait("SkipOn", "osx,linux")]
|
||||
public void UpdateCmdTemplateWithErrorsFailsValidation()
|
||||
{
|
||||
// Skip on non-Windows platforms
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: false,
|
||||
templateModifier: template =>
|
||||
{
|
||||
// Introduce syntax errors in the template
|
||||
// 1. Unbalanced parentheses
|
||||
template = template.Replace("if exist", "if exist (");
|
||||
|
||||
// 2. Unclosed quotes
|
||||
template = template.Replace("echo", "echo \"Unclosed quote");
|
||||
|
||||
return template;
|
||||
});
|
||||
}
|
||||
|
||||
private void ValidateCmdScriptTemplateSyntax(string templateName, bool shouldPass, Func<string, string> templateModifier = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var hc = new TestHostContext(this))
|
||||
{
|
||||
// Arrange
|
||||
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
|
||||
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", templateName);
|
||||
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
string tempUpdatePath = Path.Combine(tempDir, Path.GetFileName(templateName).Replace(".template", ""));
|
||||
|
||||
// Read the template
|
||||
string template = File.ReadAllText(templatePath);
|
||||
|
||||
// Apply template modifier if provided (for injecting errors)
|
||||
if (templateModifier != null)
|
||||
{
|
||||
template = templateModifier(template);
|
||||
}
|
||||
|
||||
// Replace the placeholders with valid test values
|
||||
template = template.Replace("_PROCESS_ID_", "1234");
|
||||
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener.exe");
|
||||
template = template.Replace("_ROOT_FOLDER_", rootDirectory);
|
||||
template = template.Replace("_EXIST_RUNNER_VERSION_", "2.300.0");
|
||||
template = template.Replace("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
|
||||
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
|
||||
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
|
||||
|
||||
// Write the processed template to a temporary file
|
||||
File.WriteAllText(tempUpdatePath, template);
|
||||
|
||||
// Act - Check syntax using cmd with special flags:
|
||||
// /v:on - Enable delayed environment variable expansion
|
||||
// /f:off - Disable file name completion
|
||||
// /e:on - Enable command extensions
|
||||
// These flags help validate the syntax without fully executing the script
|
||||
var process = new Process();
|
||||
process.StartInfo.FileName = "cmd.exe";
|
||||
process.StartInfo.Arguments = $"/c /v:on /f:off /e:on \"{tempUpdatePath}\" echo SyntaxCheckOnly && exit /b 0";
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.Start();
|
||||
string errors = process.StandardError.ReadToEnd();
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
// Check for mismatched parentheses in the file content
|
||||
int openParenCount = template.Split('(').Length - 1;
|
||||
int closeParenCount = template.Split(')').Length - 1;
|
||||
bool hasMissingParenthesis = openParenCount != closeParenCount;
|
||||
|
||||
// Check for unclosed quotes (simple check - not perfect but catches obvious errors)
|
||||
int doubleQuoteCount = template.Split('"').Length - 1;
|
||||
bool hasUnclosedQuotes = doubleQuoteCount % 2 != 0;
|
||||
|
||||
// Determine if the validation passed
|
||||
bool validationPassed = process.ExitCode == 0 &&
|
||||
string.IsNullOrEmpty(errors) &&
|
||||
!hasMissingParenthesis &&
|
||||
!hasUnclosedQuotes;
|
||||
|
||||
// Assert based on expected outcome
|
||||
if (shouldPass)
|
||||
{
|
||||
Assert.True(validationPassed,
|
||||
$"Template validation should have passed but failed. Exit code: {process.ExitCode}, " +
|
||||
$"Errors: {errors}, HasMissingParenthesis: {hasMissingParenthesis}, " +
|
||||
$"HasUnclosedQuotes: {hasUnclosedQuotes}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.False(validationPassed,
|
||||
"Template validation should have failed but passed. " +
|
||||
"The intentionally introduced syntax errors were not detected.");
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
try
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Assert.Fail($"Exception during test: {ex.ToString()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,7 +339,7 @@ namespace GitHub.Runner.Common.Tests
|
||||
return _traceManager[name];
|
||||
}
|
||||
|
||||
public void ShutdownRunner(ShutdownReason reason, TimeSpan delay = default)
|
||||
public void ShutdownRunner(ShutdownReason reason)
|
||||
{
|
||||
ArgUtil.NotNull(reason, nameof(reason));
|
||||
RunnerShutdownReason = reason;
|
||||
|
||||
@@ -1659,6 +1659,76 @@ runs:
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void LoadsNode24ActionDefinition()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange.
|
||||
Setup();
|
||||
const string Content = @"
|
||||
# Container action
|
||||
name: 'Hello World'
|
||||
description: 'Greet the world and record the time'
|
||||
author: 'GitHub'
|
||||
inputs:
|
||||
greeting: # id of input
|
||||
description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout'
|
||||
required: true
|
||||
default: 'Hello'
|
||||
entryPoint: # id of input
|
||||
description: 'optional docker entrypoint overwrite.'
|
||||
required: false
|
||||
outputs:
|
||||
time: # id of output
|
||||
description: 'The time we did the greeting'
|
||||
icon: 'hello.svg' # vector art to display in the GitHub Marketplace
|
||||
color: 'green' # optional, decorates the entry in the GitHub Marketplace
|
||||
runs:
|
||||
using: 'node24'
|
||||
main: 'task.js'
|
||||
";
|
||||
Pipelines.ActionStep instance;
|
||||
string directory;
|
||||
CreateAction(yamlContent: Content, instance: out instance, directory: out directory);
|
||||
|
||||
// Act.
|
||||
Definition definition = _actionManager.LoadAction(_ec.Object, instance);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(definition);
|
||||
Assert.Equal(directory, definition.Directory);
|
||||
Assert.NotNull(definition.Data);
|
||||
Assert.NotNull(definition.Data.Inputs); // inputs
|
||||
Dictionary<string, string> inputDefaults = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var input in definition.Data.Inputs)
|
||||
{
|
||||
var name = input.Key.AssertString("key").Value;
|
||||
var value = input.Value.AssertScalar("value").ToString();
|
||||
|
||||
_hc.GetTrace().Info($"Default: {name} = {value}");
|
||||
inputDefaults[name] = value;
|
||||
}
|
||||
|
||||
Assert.Equal(2, inputDefaults.Count);
|
||||
Assert.True(inputDefaults.ContainsKey("greeting"));
|
||||
Assert.Equal("Hello", inputDefaults["greeting"]);
|
||||
Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"]));
|
||||
Assert.NotNull(definition.Data.Execution); // execution
|
||||
|
||||
Assert.NotNull(definition.Data.Execution as NodeJSActionExecutionData);
|
||||
Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script);
|
||||
Assert.Equal("node24", (definition.Data.Execution as NodeJSActionExecutionData).NodeVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
@@ -2411,8 +2481,8 @@ runs:
|
||||
});
|
||||
|
||||
_launchServer = new Mock<ILaunchServer>();
|
||||
_launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>()))
|
||||
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) =>
|
||||
_launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
|
||||
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors) =>
|
||||
{
|
||||
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
|
||||
foreach (var action in actions.Actions)
|
||||
|
||||
@@ -502,6 +502,49 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Load_Node24Action()
|
||||
{
|
||||
try
|
||||
{
|
||||
//Arrange
|
||||
Setup();
|
||||
|
||||
var actionManifest = new ActionManifestManager();
|
||||
actionManifest.Initialize(_hc);
|
||||
|
||||
//Act
|
||||
var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "node24action.yml"));
|
||||
|
||||
//Assert
|
||||
Assert.Equal("Hello World", result.Name);
|
||||
Assert.Equal("Greet the world and record the time", result.Description);
|
||||
Assert.Equal(2, result.Inputs.Count);
|
||||
Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value);
|
||||
Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value);
|
||||
Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value);
|
||||
Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value);
|
||||
Assert.Equal(1, result.Deprecated.Count);
|
||||
|
||||
Assert.True(result.Deprecated.ContainsKey("greeting"));
|
||||
result.Deprecated.TryGetValue("greeting", out string value);
|
||||
Assert.Equal("This property has been deprecated", value);
|
||||
|
||||
Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType);
|
||||
|
||||
var nodeAction = result.Execution as NodeJSActionExecutionData;
|
||||
|
||||
Assert.Equal("main.js", nodeAction.Script);
|
||||
Assert.Equal("node24", nodeAction.NodeVersion);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Teardown();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
@@ -758,7 +801,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
//Assert
|
||||
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
|
||||
Assert.Contains($"Failed to load {action_path}", err.Message);
|
||||
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16' or 'node20'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
|
||||
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -1168,6 +1168,77 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void InitializeJob_HydratesJobContextWithCheckRunId()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: Create a job request message and make sure the feature flag is enabled
|
||||
var variables = new Dictionary<string, VariableValue>()
|
||||
{
|
||||
[Constants.Runner.Features.AddCheckRunIdToJobContext] = new VariableValue("true"),
|
||||
};
|
||||
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
|
||||
var pagingLogger = new Moq.Mock<IPagingLogger>();
|
||||
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
|
||||
hc.EnqueueInstance(pagingLogger.Object);
|
||||
hc.SetSingleton(jobServerQueue.Object);
|
||||
var ec = new Runner.Worker.ExecutionContext();
|
||||
ec.Initialize(hc);
|
||||
|
||||
// Arrange: Add check_run_id to the job context
|
||||
var jobContext = new Pipelines.ContextData.DictionaryContextData();
|
||||
jobContext["check_run_id"] = new NumberContextData(123456);
|
||||
jobRequest.ContextData["job"] = jobContext;
|
||||
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||
|
||||
// Act
|
||||
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(ec.JobContext);
|
||||
Assert.Equal(123456, ec.JobContext.CheckRunId);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this test can be deleted when `AddCheckRunIdToJobContext` is fully rolled out
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void InitializeJob_HydratesJobContextWithCheckRunId_FeatureFlagDisabled()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: Create a job request message and make sure the feature flag is disabled
|
||||
var variables = new Dictionary<string, VariableValue>()
|
||||
{
|
||||
[Constants.Runner.Features.AddCheckRunIdToJobContext] = new VariableValue("false"),
|
||||
};
|
||||
var jobRequest = new Pipelines.AgentJobRequestMessage(new TaskOrchestrationPlanReference(), new TimelineReference(), Guid.NewGuid(), "some job name", "some job name", null, null, null, variables, new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null, null);
|
||||
var pagingLogger = new Moq.Mock<IPagingLogger>();
|
||||
var jobServerQueue = new Moq.Mock<IJobServerQueue>();
|
||||
hc.EnqueueInstance(pagingLogger.Object);
|
||||
hc.SetSingleton(jobServerQueue.Object);
|
||||
var ec = new Runner.Worker.ExecutionContext();
|
||||
ec.Initialize(hc);
|
||||
|
||||
// Arrange: Add check_run_id to the job context
|
||||
var jobContext = new Pipelines.ContextData.DictionaryContextData();
|
||||
jobContext["check_run_id"] = new NumberContextData(123456);
|
||||
jobRequest.ContextData["job"] = jobContext;
|
||||
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||
|
||||
// Act
|
||||
ec.InitializeJob(jobRequest, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(ec.JobContext);
|
||||
Assert.Null(ec.JobContext.CheckRunId); // with the feature flag disabled we should not have added a CheckRunId to the JobContext
|
||||
}
|
||||
}
|
||||
|
||||
private bool ExpressionValuesAssertEqual(DictionaryContextData expect, DictionaryContextData actual)
|
||||
{
|
||||
foreach (var key in expect.Keys.ToList())
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[InlineData("node12", "node20")]
|
||||
[InlineData("node16", "node20")]
|
||||
[InlineData("node20", "node20")]
|
||||
[InlineData("node24", "node24")]
|
||||
public void IsNodeVersionUpgraded(string inputVersion, string expectedVersion)
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
@@ -41,7 +42,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
var hf = new HandlerFactory();
|
||||
hf.Initialize(hc);
|
||||
|
||||
// Server Feature Flag
|
||||
// Setup variables
|
||||
var variables = new Dictionary<string, VariableValue>();
|
||||
Variables serverVariables = new(hc, variables);
|
||||
|
||||
@@ -72,5 +73,48 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
Assert.Equal(expectedVersion, handler.Data.NodeVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void Node24ExplicitlyRequested_HonoredByDefault()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange.
|
||||
var hf = new HandlerFactory();
|
||||
hf.Initialize(hc);
|
||||
|
||||
// Basic variables setup
|
||||
var variables = new Dictionary<string, VariableValue>();
|
||||
Variables serverVariables = new(hc, variables);
|
||||
|
||||
_ec.Setup(x => x.Global).Returns(new GlobalContext()
|
||||
{
|
||||
Variables = serverVariables,
|
||||
EnvironmentVariables = new Dictionary<string, string>()
|
||||
});
|
||||
|
||||
// Act - Node 24 explicitly requested in action.yml
|
||||
var data = new NodeJSActionExecutionData();
|
||||
data.NodeVersion = "node24";
|
||||
var handler = hf.Create(
|
||||
_ec.Object,
|
||||
new ScriptReference(),
|
||||
new Mock<IStepHost>().Object,
|
||||
data,
|
||||
new Dictionary<string, string>(),
|
||||
new Dictionary<string, string>(),
|
||||
new Variables(hc, new Dictionary<string, VariableValue>()),
|
||||
"",
|
||||
new List<JobExtensionRunner>()
|
||||
) as INodeScriptActionHandler;
|
||||
|
||||
// Assert - should be node24 as requested
|
||||
Assert.Equal("node24", handler.Data.NodeVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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("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]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
|
||||
@@ -162,6 +162,60 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
Assert.Equal("node20", nodeVersion);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task DetermineNode24RuntimeVersionInAlpineContainerAsync()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange.
|
||||
var sh = new ContainerStepHost();
|
||||
sh.Initialize(hc);
|
||||
sh.Container = new ContainerInfo() { ContainerId = "1234abcd" };
|
||||
|
||||
_dc.Setup(d => d.DockerExec(_ec.Object, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<List<string>>()))
|
||||
.Callback((IExecutionContext ec, string id, string options, string command, List<string> output) =>
|
||||
{
|
||||
output.Add("alpine");
|
||||
})
|
||||
.ReturnsAsync(0);
|
||||
|
||||
// Act.
|
||||
var nodeVersion = await sh.DetermineNodeRuntimeVersion(_ec.Object, "node24");
|
||||
|
||||
// Assert.
|
||||
Assert.Equal("node24_alpine", nodeVersion);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task DetermineNode24RuntimeVersionInUnknownContainerAsync()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange.
|
||||
var sh = new ContainerStepHost();
|
||||
sh.Initialize(hc);
|
||||
sh.Container = new ContainerInfo() { ContainerId = "1234abcd" };
|
||||
|
||||
_dc.Setup(d => d.DockerExec(_ec.Object, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<List<string>>()))
|
||||
.Callback((IExecutionContext ec, string id, string options, string command, List<string> output) =>
|
||||
{
|
||||
output.Add("github");
|
||||
})
|
||||
.ReturnsAsync(0);
|
||||
|
||||
// Act.
|
||||
var nodeVersion = await sh.DetermineNodeRuntimeVersion(_ec.Object, "node24");
|
||||
|
||||
// Assert.
|
||||
Assert.Equal("node24", nodeVersion);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="xunit" Version="2.7.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />
|
||||
<PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
|
||||
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"
|
||||
PACKAGE_DIR="$SCRIPT_DIR/../_package"
|
||||
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
|
||||
DOTNETSDK_VERSION="8.0.408"
|
||||
DOTNETSDK_VERSION="8.0.412"
|
||||
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
|
||||
RUNNER_VERSION=$(cat runnerversion)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.408"
|
||||
"version": "8.0.412"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.323.0
|
||||
2.327.0
|
||||
|
||||
Reference in New Issue
Block a user