mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
14 Commits
salmanmkc/
...
chore/npm-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
945b418b51 | ||
|
|
60af948051 | ||
|
|
ff775ca101 | ||
|
|
f74be39e77 | ||
|
|
1eb15f28a7 | ||
|
|
afe4fc8446 | ||
|
|
a12731d34d | ||
|
|
18f2450d71 | ||
|
|
2c5f29c3ca | ||
|
|
c9de9a8699 | ||
|
|
68ff57dbc4 | ||
|
|
c774eb8d46 | ||
|
|
f184048a9a | ||
|
|
338d83a941 |
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
@@ -38,4 +38,4 @@ jobs:
|
||||
working-directory: src
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
2
.github/workflows/dependency-check.yml
vendored
2
.github/workflows/dependency-check.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
|
||||
84
.github/workflows/node-upgrade.yml
vendored
84
.github/workflows/node-upgrade.yml
vendored
@@ -32,20 +32,47 @@ jobs:
|
||||
echo "Verifying availability in alpine_nodejs..."
|
||||
ALPINE_RELEASES=$(curl -s https://api.github.com/repos/actions/alpine_nodejs/releases | jq -r '.[].tag_name')
|
||||
|
||||
if ! echo "$ALPINE_RELEASES" | grep -q "^node20-$LATEST_NODE20$"; then
|
||||
if ! echo "$ALPINE_RELEASES" | grep -q "^v$LATEST_NODE20$"; then
|
||||
echo "::warning title=Node 20 Fallback::Node 20 version $LATEST_NODE20 not found in alpine_nodejs releases, using fallback"
|
||||
# Fall back to latest available alpine_nodejs v20 release
|
||||
LATEST_NODE20=$(echo "$ALPINE_RELEASES" | grep "^node20-" | head -1 | sed 's/^node20-//')
|
||||
LATEST_NODE20=$(echo "$ALPINE_RELEASES" | grep "^v20\." | head -1 | sed 's/^v//')
|
||||
echo "Using latest available alpine_nodejs Node 20: $LATEST_NODE20"
|
||||
fi
|
||||
|
||||
if ! echo "$ALPINE_RELEASES" | grep -q "^node24-$LATEST_NODE24$"; then
|
||||
if ! echo "$ALPINE_RELEASES" | grep -q "^v$LATEST_NODE24$"; then
|
||||
echo "::warning title=Node 24 Fallback::Node 24 version $LATEST_NODE24 not found in alpine_nodejs releases, using fallback"
|
||||
# Fall back to latest available alpine_nodejs v24 release
|
||||
LATEST_NODE24=$(echo "$ALPINE_RELEASES" | grep "^node24-" | head -1 | sed 's/^node24-//')
|
||||
LATEST_NODE24=$(echo "$ALPINE_RELEASES" | grep "^v24\." | head -1 | sed 's/^v//')
|
||||
echo "Using latest available alpine_nodejs Node 24: $LATEST_NODE24"
|
||||
fi
|
||||
|
||||
# Validate that we have non-empty version numbers
|
||||
if [ -z "$LATEST_NODE20" ] || [ "$LATEST_NODE20" = "" ]; then
|
||||
echo "::error title=Invalid Node 20 Version::Failed to determine valid Node 20 version. Got: '$LATEST_NODE20'"
|
||||
echo "Available alpine_nodejs releases:"
|
||||
echo "$ALPINE_RELEASES" | head -10
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$LATEST_NODE24" ] || [ "$LATEST_NODE24" = "" ]; then
|
||||
echo "::error title=Invalid Node 24 Version::Failed to determine valid Node 24 version. Got: '$LATEST_NODE24'"
|
||||
echo "Available alpine_nodejs releases:"
|
||||
echo "$ALPINE_RELEASES" | head -10
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Additional validation: ensure versions match expected format (x.y.z)
|
||||
if ! echo "$LATEST_NODE20" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error title=Invalid Node 20 Format::Node 20 version '$LATEST_NODE20' does not match expected format (x.y.z)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$LATEST_NODE24" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error title=Invalid Node 24 Format::Node 24 version '$LATEST_NODE24' does not match expected format (x.y.z)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Validated Node versions: 20=$LATEST_NODE20, 24=$LATEST_NODE24"
|
||||
echo "latest_node20=$LATEST_NODE20" >> $GITHUB_OUTPUT
|
||||
echo "latest_node24=$LATEST_NODE24" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -82,13 +109,50 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Final validation before making changes
|
||||
NODE20_VERSION="${{ steps.node-versions.outputs.latest_node20 }}"
|
||||
NODE24_VERSION="${{ steps.node-versions.outputs.latest_node24 }}"
|
||||
|
||||
echo "Final validation of versions before PR creation:"
|
||||
echo "Node 20: '$NODE20_VERSION'"
|
||||
echo "Node 24: '$NODE24_VERSION'"
|
||||
|
||||
# Validate versions are not empty and match expected format
|
||||
if [ -z "$NODE20_VERSION" ] || ! echo "$NODE20_VERSION" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error title=Invalid Node 20 Version::Refusing to create PR with invalid Node 20 version: '$NODE20_VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$NODE24_VERSION" ] || ! echo "$NODE24_VERSION" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error title=Invalid Node 24 Version::Refusing to create PR with invalid Node 24 version: '$NODE24_VERSION'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All versions validated successfully"
|
||||
|
||||
# Update the files
|
||||
if [ "${{ steps.node-versions.outputs.needs_update20 }}" == "true" ]; then
|
||||
sed -i 's/NODE20_VERSION="[^"]*"/NODE20_VERSION="${{ steps.node-versions.outputs.latest_node20 }}"/' src/Misc/externals.sh
|
||||
sed -i 's/NODE20_VERSION="[^"]*"/NODE20_VERSION="'"$NODE20_VERSION"'"/' src/Misc/externals.sh
|
||||
fi
|
||||
|
||||
if [ "${{ steps.node-versions.outputs.needs_update24 }}" == "true" ]; then
|
||||
sed -i 's/NODE24_VERSION="[^"]*"/NODE24_VERSION="${{ steps.node-versions.outputs.latest_node24 }}"/' src/Misc/externals.sh
|
||||
sed -i 's/NODE24_VERSION="[^"]*"/NODE24_VERSION="'"$NODE24_VERSION"'"/' src/Misc/externals.sh
|
||||
fi
|
||||
|
||||
# Verify the changes were applied correctly
|
||||
echo "Verifying changes in externals.sh:"
|
||||
grep "NODE20_VERSION=" src/Misc/externals.sh
|
||||
grep "NODE24_VERSION=" src/Misc/externals.sh
|
||||
|
||||
# Ensure we actually have valid versions in the file
|
||||
UPDATED_NODE20=$(grep "NODE20_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
|
||||
UPDATED_NODE24=$(grep "NODE24_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
|
||||
|
||||
if [ -z "$UPDATED_NODE20" ] || [ -z "$UPDATED_NODE24" ]; then
|
||||
echo "::error title=Update Failed::Failed to properly update externals.sh"
|
||||
echo "Updated Node 20: '$UPDATED_NODE20'"
|
||||
echo "Updated Node 24: '$UPDATED_NODE24'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure git
|
||||
@@ -98,15 +162,15 @@ jobs:
|
||||
# Create branch and commit changes
|
||||
branch_name="chore/update-node"
|
||||
git checkout -b "$branch_name"
|
||||
git commit -a -m "chore: update Node versions (20: ${{ steps.node-versions.outputs.latest_node20 }}, 24: ${{ steps.node-versions.outputs.latest_node24 }})"
|
||||
git commit -a -m "chore: update Node versions (20: $NODE20_VERSION, 24: $NODE24_VERSION)"
|
||||
git push --force origin "$branch_name"
|
||||
|
||||
# Create PR body using here-doc for proper formatting
|
||||
cat > pr_body.txt << 'EOF'
|
||||
cat > pr_body.txt << EOF
|
||||
Automated Node.js version update:
|
||||
|
||||
- Node 20: ${{ steps.node-versions.outputs.current_node20 }} → ${{ steps.node-versions.outputs.latest_node20 }}
|
||||
- Node 24: ${{ steps.node-versions.outputs.current_node24 }} → ${{ steps.node-versions.outputs.latest_node24 }}
|
||||
- Node 20: ${{ steps.node-versions.outputs.current_node20 }} → $NODE20_VERSION
|
||||
- Node 24: ${{ steps.node-versions.outputs.current_node24 }} → $NODE24_VERSION
|
||||
|
||||
This update ensures we're using the latest stable Node.js versions for security and performance improvements.
|
||||
|
||||
|
||||
2
.github/workflows/npm-audit-typescript.yml
vendored
2
.github/workflows/npm-audit-typescript.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: NPM install and audit fix with TypeScript auto-repair
|
||||
|
||||
2
.github/workflows/npm-audit.yml
vendored
2
.github/workflows/npm-audit.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG RUNNER_VERSION
|
||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||
ARG DOCKER_VERSION=28.4.0
|
||||
ARG BUILDX_VERSION=0.28.0
|
||||
ARG DOCKER_VERSION=28.5.1
|
||||
ARG BUILDX_VERSION=0.29.1
|
||||
|
||||
RUN apt update -y && apt install curl unzip -y
|
||||
|
||||
@@ -21,6 +21,10 @@ RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-c
|
||||
&& unzip ./runner-container-hooks.zip -d ./k8s \
|
||||
&& rm runner-container-hooks.zip
|
||||
|
||||
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v0.8.0/actions-runner-hooks-k8s-0.8.0.zip \
|
||||
&& unzip ./runner-container-hooks.zip -d ./k8s-novolume \
|
||||
&& rm runner-container-hooks.zip
|
||||
|
||||
RUN export RUNNER_ARCH=${TARGETARCH} \
|
||||
&& if [ "$RUNNER_ARCH" = "amd64" ]; then export DOCKER_ARCH=x86_64 ; fi \
|
||||
&& if [ "$RUNNER_ARCH" = "arm64" ]; then export DOCKER_ARCH=aarch64 ; fi \
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
## What's Changed
|
||||
* Update Docker to v28.3.2 and Buildx to v0.26.1 by @github-actions[bot] in https://github.com/actions/runner/pull/3953
|
||||
* Fix if statement structure in update script and variable reference by @salmanmkc in https://github.com/actions/runner/pull/3956
|
||||
* Add V2 flow for runner deletion by @Samirat in https://github.com/actions/runner/pull/3954
|
||||
* Node 20 -> Node 24 migration feature flagging, opt-in and opt-out environment variables by @salmanmkc in https://github.com/actions/runner/pull/3948
|
||||
* Update Node20 and Node24 to latest by @djs-intel in https://github.com/actions/runner/pull/3972
|
||||
* Redirect supported OS doc section to current public Docs location by @corycalahan in https://github.com/actions/runner/pull/3979
|
||||
* Bump Microsoft.NET.Test.Sdk from 17.13.0 to 17.14.1 by @dependabot[bot] in https://github.com/actions/runner/pull/3975
|
||||
* Bump Azure.Storage.Blobs from 12.24.0 to 12.25.0 by @dependabot[bot] in https://github.com/actions/runner/pull/3974
|
||||
* Bump actions/download-artifact from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/3973
|
||||
* Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/3982
|
||||
* Update safe_sleep.sh for bug when scheduler is paused for more than 1 second by @horner in https://github.com/actions/runner/pull/3157
|
||||
* Acknowledge runner request by @ericsciple in https://github.com/actions/runner/pull/3996
|
||||
* Update Docker to v28.3.3 and Buildx to v0.27.0 by @github-actions[bot] in https://github.com/actions/runner/pull/3999
|
||||
* Update dotnet sdk to latest version @8.0.413 by @github-actions[bot] in https://github.com/actions/runner/pull/4000
|
||||
* Bump actions/attest-build-provenance from 2 to 3 by @dependabot[bot] in https://github.com/actions/runner/pull/4002
|
||||
* Bump @typescript-eslint/eslint-plugin from 6.7.2 to 8.35.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/3920
|
||||
* Bump husky from 8.0.3 to 9.1.7 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/3842
|
||||
* Bump @vercel/ncc from 0.38.0 to 0.38.3 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/3841
|
||||
* Bump eslint-plugin-github from 4.10.0 to 4.10.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/3180
|
||||
* Bump typescript from 5.2.2 to 5.9.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4007
|
||||
* chore: migrate Husky config from v8 to v9 format by @salmanmkc in https://github.com/actions/runner/pull/4003
|
||||
* Map RUNNER_TEMP for container action by @ericsciple in https://github.com/actions/runner/pull/4011
|
||||
* Break UseV2Flow into UseV2Flow and UseRunnerAdminFlow. by @TingluoHuang in https://github.com/actions/runner/pull/4013
|
||||
* Update Docker to v28.4.0 and Buildx to v0.28.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4020
|
||||
* Bump node.js to latest version in runner. by @TingluoHuang in https://github.com/actions/runner/pull/4022
|
||||
* feat: add automated .NET dependency management workflow by @salmanmkc in https://github.com/actions/runner/pull/4028
|
||||
* feat: add automated Docker BuildX dependency management workflow by @salmanmkc in https://github.com/actions/runner/pull/4029
|
||||
* feat: add automated Node.js version management workflow by @salmanmkc in https://github.com/actions/runner/pull/4026
|
||||
* feat: add comprehensive NPM security management workflow by @salmanmkc in https://github.com/actions/runner/pull/4027
|
||||
* feat: add comprehensive dependency monitoring system by @salmanmkc in https://github.com/actions/runner/pull/4025
|
||||
* Use BrokerURL when using RunnerAdmin by @luketomlinson in https://github.com/actions/runner/pull/4044
|
||||
* Bump actions/github-script from 7.0.1 to 8.0.0 by @dependabot[bot] in https://github.com/actions/runner/pull/4016
|
||||
* Bump actions/stale from 9 to 10 by @dependabot[bot] in https://github.com/actions/runner/pull/4015
|
||||
* fix: prevent Node.js upgrade workflow from creating PRs with empty versions by @salmanmkc in https://github.com/actions/runner/pull/4055
|
||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4057
|
||||
* Bump actions/setup-node from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/4037
|
||||
* Bump Azure.Storage.Blobs from 12.25.0 to 12.25.1 by @dependabot[bot] in https://github.com/actions/runner/pull/4058
|
||||
* Update Docker to v28.5.0 and Buildx to v0.29.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4069
|
||||
* Bump github/codeql-action from 3 to 4 by @dependabot[bot] in https://github.com/actions/runner/pull/4072
|
||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4075
|
||||
* Include k8s novolume (version v0.8.0) by @nikola-jokic in https://github.com/actions/runner/pull/4063
|
||||
* Make sure runner-admin has both auth_url and auth_url_v2. by @TingluoHuang in https://github.com/actions/runner/pull/4066
|
||||
* Report job has infra failure to run-service by @TingluoHuang in https://github.com/actions/runner/pull/4073
|
||||
* Bump actions/setup-node from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4078
|
||||
|
||||
## New Contributors
|
||||
* @Samirat made their first contribution in https://github.com/actions/runner/pull/3954
|
||||
* @djs-intel made their first contribution in https://github.com/actions/runner/pull/3972
|
||||
* @horner made their first contribution in https://github.com/actions/runner/pull/3157
|
||||
|
||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.327.1...v2.328.0
|
||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.328.0...v2.329.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
@@ -1815,10 +1815,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -5904,9 +5905,9 @@
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ 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.5"
|
||||
NODE24_VERSION="24.7.0"
|
||||
NODE24_VERSION="24.10.0"
|
||||
|
||||
get_abs_path() {
|
||||
# exploits the fact that pwd will print abs path when no args
|
||||
|
||||
@@ -170,6 +170,8 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
|
||||
public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors";
|
||||
public static readonly string ContainerActionRunnerTemp = "actions_container_action_runner_temp";
|
||||
public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_check";
|
||||
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
|
||||
}
|
||||
|
||||
// Node version migration related constants
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace GitHub.Runner.Common
|
||||
string environmentUrl,
|
||||
IList<Telemetry> telemetry,
|
||||
string billingOwnerId,
|
||||
string infrastructureFailureCategory,
|
||||
CancellationToken token);
|
||||
|
||||
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
||||
@@ -80,11 +81,12 @@ namespace GitHub.Runner.Common
|
||||
string environmentUrl,
|
||||
IList<Telemetry> telemetry,
|
||||
string billingOwnerId,
|
||||
string infrastructureFailureCategory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
CheckConnection();
|
||||
return RetryRequest(
|
||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, environmentUrl, telemetry, billingOwnerId, cancellationToken), cancellationToken,
|
||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, environmentUrl, telemetry, billingOwnerId, infrastructureFailureCategory, cancellationToken), cancellationToken,
|
||||
shouldRetry: ex =>
|
||||
ex is not VssUnauthorizedException && // HTTP status 401
|
||||
ex is not TaskOrchestrationJobNotFoundException); // HTTP status 404
|
||||
|
||||
@@ -284,6 +284,7 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
{
|
||||
var runner = await _dotcomServer.ReplaceRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
|
||||
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
|
||||
runnerSettings.UseV2Flow = true; // if we are using runner admin, we also need to hit broker
|
||||
|
||||
agent.Id = runner.Id;
|
||||
agent.Authorization = new TaskAgentAuthorization()
|
||||
@@ -291,6 +292,13 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
AuthorizationUrl = runner.RunnerAuthorization.AuthorizationUrl,
|
||||
ClientId = new Guid(runner.RunnerAuthorization.ClientId)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(runner.RunnerAuthorization.LegacyAuthorizationUrl?.AbsoluteUri))
|
||||
{
|
||||
agent.Authorization.AuthorizationUrl = runner.RunnerAuthorization.LegacyAuthorizationUrl;
|
||||
agent.Properties["EnableAuthMigrationByDefault"] = true;
|
||||
agent.Properties["AuthorizationUrlV2"] = runner.RunnerAuthorization.AuthorizationUrl.AbsoluteUri;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -342,6 +350,13 @@ namespace GitHub.Runner.Listener.Configuration
|
||||
AuthorizationUrl = runner.RunnerAuthorization.AuthorizationUrl,
|
||||
ClientId = new Guid(runner.RunnerAuthorization.ClientId)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(runner.RunnerAuthorization.LegacyAuthorizationUrl?.AbsoluteUri))
|
||||
{
|
||||
agent.Authorization.AuthorizationUrl = runner.RunnerAuthorization.LegacyAuthorizationUrl;
|
||||
agent.Properties["EnableAuthMigrationByDefault"] = true;
|
||||
agent.Properties["AuthorizationUrlV2"] = runner.RunnerAuthorization.AuthorizationUrl.AbsoluteUri;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1211,7 +1211,7 @@ namespace GitHub.Runner.Listener
|
||||
jobAnnotations.Add(annotation.Value);
|
||||
}
|
||||
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, environmentUrl: null, telemetry: null, billingOwnerId: message.BillingOwnerId, CancellationToken.None);
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, environmentUrl: null, telemetry: null, billingOwnerId: message.BillingOwnerId, infrastructureFailureCategory: null, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
// Log the error and fail the PrepareActionsAsync Initialization.
|
||||
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
||||
executionContext.InfrastructureError(ex.Message);
|
||||
executionContext.InfrastructureError(ex.Message, category: "resolve_action");
|
||||
executionContext.Result = TaskResult.Failed;
|
||||
throw;
|
||||
}
|
||||
@@ -119,7 +119,7 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
// Log the error and fail the PrepareActionsAsync Initialization.
|
||||
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
||||
executionContext.InfrastructureError(ex.Message);
|
||||
executionContext.InfrastructureError(ex.Message, category: "invalid_action_download");
|
||||
executionContext.Result = TaskResult.Failed;
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -111,19 +111,19 @@ namespace GitHub.Runner.Worker.Container
|
||||
{
|
||||
IList<string> dockerOptions = new List<string>();
|
||||
// OPTIONS
|
||||
dockerOptions.Add(DockerUtil.CreateEscapedOption("--name", container.ContainerDisplayName));
|
||||
dockerOptions.Add($"--name {container.ContainerDisplayName}");
|
||||
dockerOptions.Add($"--label {DockerInstanceLabel}");
|
||||
if (!string.IsNullOrEmpty(container.ContainerWorkDirectory))
|
||||
{
|
||||
dockerOptions.Add(DockerUtil.CreateEscapedOption("--workdir", container.ContainerWorkDirectory));
|
||||
dockerOptions.Add($"--workdir {container.ContainerWorkDirectory}");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(container.ContainerNetwork))
|
||||
{
|
||||
dockerOptions.Add(DockerUtil.CreateEscapedOption("--network", container.ContainerNetwork));
|
||||
dockerOptions.Add($"--network {container.ContainerNetwork}");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(container.ContainerNetworkAlias))
|
||||
{
|
||||
dockerOptions.Add(DockerUtil.CreateEscapedOption("--network-alias", container.ContainerNetworkAlias));
|
||||
dockerOptions.Add($"--network-alias {container.ContainerNetworkAlias}");
|
||||
}
|
||||
foreach (var port in container.UserPortMappings)
|
||||
{
|
||||
@@ -195,10 +195,10 @@ namespace GitHub.Runner.Worker.Container
|
||||
{
|
||||
IList<string> dockerOptions = new List<string>();
|
||||
// OPTIONS
|
||||
dockerOptions.Add(DockerUtil.CreateEscapedOption("--name", container.ContainerDisplayName));
|
||||
dockerOptions.Add($"--name {container.ContainerDisplayName}");
|
||||
dockerOptions.Add($"--label {DockerInstanceLabel}");
|
||||
|
||||
dockerOptions.Add(DockerUtil.CreateEscapedOption("--workdir", container.ContainerWorkDirectory));
|
||||
dockerOptions.Add($"--workdir {container.ContainerWorkDirectory}");
|
||||
dockerOptions.Add($"--rm");
|
||||
|
||||
foreach (var env in container.ContainerEnvironmentVariables)
|
||||
|
||||
@@ -522,6 +522,10 @@ namespace GitHub.Runner.Worker
|
||||
if (annotation != null)
|
||||
{
|
||||
stepResult.Annotations.Add(annotation.Value);
|
||||
if (annotation.Value.IsInfrastructureIssue && string.IsNullOrEmpty(Global.InfrastructureFailureCategory))
|
||||
{
|
||||
Global.InfrastructureFailureCategory = issue.Category;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1335,9 +1339,9 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
// Do not add a format string overload. See comment on ExecutionContext.Write().
|
||||
public static void InfrastructureError(this IExecutionContext context, string message)
|
||||
public static void InfrastructureError(this IExecutionContext context, string message, string category = null)
|
||||
{
|
||||
var issue = new Issue() { Type = IssueType.Error, Message = message, IsInfrastructureIssue = true };
|
||||
var issue = new Issue() { Type = IssueType.Error, Message = message, IsInfrastructureIssue = true, Category = category };
|
||||
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace GitHub.Runner.Worker
|
||||
public StepsContext StepsContext { get; set; }
|
||||
public Variables Variables { get; set; }
|
||||
public bool WriteDebug { get; set; }
|
||||
public string InfrastructureFailureCategory { get; set; }
|
||||
public JObject ContainerHookState { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
{
|
||||
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
|
||||
scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
|
||||
resolvedScriptPath = $"\"{StepHost.ResolvePathForStepHost(ExecutionContext, scriptFilePath).Replace("\"", "\\\"")}\"";
|
||||
resolvedScriptPath = StepHost.ResolvePathForStepHost(ExecutionContext, scriptFilePath).Replace("\"", "\\\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -260,7 +260,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
}
|
||||
scriptFilePath = Inputs["path"];
|
||||
ArgUtil.NotNullOrEmpty(scriptFilePath, "path");
|
||||
resolvedScriptPath = $"\"{Inputs["path"].Replace("\"", "\\\"")}\"";
|
||||
resolvedScriptPath = Inputs["path"].Replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
// Format arg string with script path
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
@@ -64,47 +63,10 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
var append = @"if ((Test-Path -LiteralPath variable:\LASTEXITCODE)) { exit $LASTEXITCODE }";
|
||||
contents = $"{prepend}{Environment.NewLine}{contents}{Environment.NewLine}{append}";
|
||||
break;
|
||||
case "bash":
|
||||
case "sh":
|
||||
contents = FixBashEnvironmentVariables(contents);
|
||||
break;
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixes unquoted environment variables in bash/sh scripts to prevent issues with paths containing spaces.
|
||||
/// This method quotes environment variables used in shell redirects and command substitutions.
|
||||
/// </summary>
|
||||
/// <param name="contents">The shell script content to fix</param>
|
||||
/// <returns>Fixed shell script content with properly quoted environment variables</returns>
|
||||
private static string FixBashEnvironmentVariables(string contents)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contents))
|
||||
{
|
||||
return contents;
|
||||
}
|
||||
|
||||
// Pattern to match environment variables in shell redirects that aren't already quoted
|
||||
// This targets patterns like: >> $GITHUB_STEP_SUMMARY, > $GITHUB_OUTPUT, etc.
|
||||
// but avoids already quoted ones like: >> "$GITHUB_STEP_SUMMARY" or >> '$GITHUB_OUTPUT'
|
||||
var redirectPattern = new Regex(
|
||||
@"(\s+(?:>>|>|<|2>>|2>)\s+)(\$[A-Za-z_][A-Za-z0-9_]*)\b(?!\s*['""])",
|
||||
RegexOptions.Compiled | RegexOptions.Multiline
|
||||
);
|
||||
|
||||
// Replace unquoted environment variables in redirects with quoted versions
|
||||
contents = redirectPattern.Replace(contents, match =>
|
||||
{
|
||||
var redirectOperator = match.Groups[1].Value; // e.g., " >> "
|
||||
var envVar = match.Groups[2].Value; // e.g., "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
return $"{redirectOperator}\"{envVar}\"";
|
||||
});
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
internal static (string shellCommand, string shellArgs) ParseShellOptionString(string shellOption)
|
||||
{
|
||||
var shellStringParts = shellOption.Split(" ", 2);
|
||||
|
||||
@@ -220,7 +220,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
// [OPTIONS]
|
||||
dockerCommandArgs.Add($"-i");
|
||||
dockerCommandArgs.Add(DockerUtil.CreateEscapedOption("--workdir", workingDirectory));
|
||||
dockerCommandArgs.Add($"--workdir {workingDirectory}");
|
||||
foreach (var env in environment)
|
||||
{
|
||||
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
|
||||
|
||||
@@ -400,6 +400,10 @@ namespace GitHub.Runner.Worker
|
||||
if (snapshotRequest != null)
|
||||
{
|
||||
var snapshotOperationProvider = HostContext.GetService<ISnapshotOperationProvider>();
|
||||
// Check that that runner is capable of taking a snapshot
|
||||
snapshotOperationProvider.RunSnapshotPreflightChecks(context);
|
||||
|
||||
// Add postjob step to write snapshot file
|
||||
jobContext.RegisterPostJobStep(new JobExtensionRunner(
|
||||
runAsync: (executionContext, _) => snapshotOperationProvider.CreateSnapshotRequestAsync(executionContext, snapshotRequest),
|
||||
condition: snapshotRequest.Condition,
|
||||
|
||||
@@ -321,7 +321,7 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
try
|
||||
{
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, default);
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, infrastructureFailureCategory: jobContext.Global.InfrastructureFailureCategory, default);
|
||||
return result;
|
||||
}
|
||||
catch (VssUnauthorizedException ex)
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
<PublishReadyToRunComposite>true</PublishReadyToRunComposite>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Test</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sdk\Sdk.csproj" />
|
||||
<ProjectReference Include="..\Runner.Common\Runner.Common.csproj" />
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Handlers;
|
||||
namespace GitHub.Runner.Worker;
|
||||
|
||||
[ServiceLocator(Default = typeof(SnapshotOperationProvider))]
|
||||
public interface ISnapshotOperationProvider : IRunnerService
|
||||
{
|
||||
Task CreateSnapshotRequestAsync(IExecutionContext executionContext, Snapshot snapshotRequest);
|
||||
void RunSnapshotPreflightChecks(IExecutionContext jobContext);
|
||||
}
|
||||
|
||||
public class SnapshotOperationProvider : RunnerService, ISnapshotOperationProvider
|
||||
@@ -24,9 +28,32 @@ public class SnapshotOperationProvider : RunnerService, ISnapshotOperationProvid
|
||||
}
|
||||
|
||||
IOUtil.SaveObject(snapshotRequest, snapshotRequestFilePath);
|
||||
executionContext.Output($"Image Name: {snapshotRequest.ImageName} Version: {snapshotRequest.Version}");
|
||||
executionContext.Output($"Request written to: {snapshotRequestFilePath}");
|
||||
executionContext.Output("This request will be processed after the job completes. You will not receive any feedback on the snapshot process within the workflow logs of this job.");
|
||||
executionContext.Output("If the snapshot process is successful, you should see a new image with the requested name in the list of available custom images when creating a new GitHub-hosted Runner.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void RunSnapshotPreflightChecks(IExecutionContext context)
|
||||
{
|
||||
var shouldCheckRunnerEnvironment = context.Global.Variables.GetBoolean(Constants.Runner.Features.SnapshotPreflightHostedRunnerCheck) ?? false;
|
||||
if (shouldCheckRunnerEnvironment &&
|
||||
context.Global.Variables.TryGetValue(WellKnownDistributedTaskVariables.RunnerEnvironment, out var runnerEnvironment) &&
|
||||
!string.IsNullOrEmpty(runnerEnvironment))
|
||||
{
|
||||
context.Debug($"Snapshot: RUNNER_ENVIRONMENT={runnerEnvironment}");
|
||||
if (!string.Equals(runnerEnvironment, "github-hosted", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Snapshot workflows must be run on a GitHub Hosted Runner");
|
||||
}
|
||||
}
|
||||
var imageGenEnabled = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED"));
|
||||
context.Debug($"Snapshot: GITHUB_ACTIONS_IMAGE_GEN_ENABLED={imageGenEnabled}");
|
||||
var shouldCheckImageGenPool = context.Global.Variables.GetBoolean(Constants.Runner.Features.SnapshotPreflightImageGenPoolCheck) ?? false;
|
||||
if (shouldCheckImageGenPool && !imageGenEnabled)
|
||||
{
|
||||
throw new ArgumentException("Snapshot workflows must be run a hosted runner with Image Generation enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,16 @@ namespace GitHub.DistributedTask.WebApi
|
||||
internal set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The url to refresh tokens with legacy service
|
||||
/// </summary>
|
||||
[JsonProperty("legacy_authorization_url")]
|
||||
public Uri LegacyAuthorizationUrl
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The url to connect to poll for messages
|
||||
/// </summary>
|
||||
|
||||
@@ -35,5 +35,8 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
|
||||
[DataMember(Name = "billingOwnerId", EmitDefaultValue = false)]
|
||||
public string BillingOwnerId { get; set; }
|
||||
|
||||
[DataMember(Name = "infrastructureFailureCategory", EmitDefaultValue = false)]
|
||||
public string InfrastructureFailureCategory { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace Sdk.RSWebApi.Contracts
|
||||
StartColumn = columnNumber,
|
||||
EndColumn = endColumnNumber,
|
||||
StepNumber = stepNumber,
|
||||
IsInfrastructureIssue = issue.IsInfrastructureIssue ?? false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
string environmentUrl,
|
||||
IList<Telemetry> telemetry,
|
||||
string billingOwnerId,
|
||||
string infrastructureFailureCategory,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpMethod httpMethod = new HttpMethod("POST");
|
||||
@@ -145,6 +146,7 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
EnvironmentUrl = environmentUrl,
|
||||
Telemetry = telemetry,
|
||||
BillingOwnerId = billingOwnerId,
|
||||
InfrastructureFailureCategory = infrastructureFailureCategory
|
||||
};
|
||||
|
||||
requestUri = new Uri(requestUri, "completejob");
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.25.0" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.25.1" />
|
||||
<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" />
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Handlers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker.Handlers
|
||||
{
|
||||
public sealed class ScriptHandlerL0
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ScriptPath_WithSpaces_ShouldBeQuoted()
|
||||
{
|
||||
// Arrange - Test the path quoting logic that our fix addresses
|
||||
var tempPathWithSpaces = "/path with spaces/_temp";
|
||||
var scriptPathWithSpaces = Path.Combine(tempPathWithSpaces, "test-script.sh");
|
||||
|
||||
// Test the original (broken) behavior
|
||||
var originalPath = scriptPathWithSpaces.Replace("\"", "\\\"");
|
||||
|
||||
// Test our fix - properly quoted path
|
||||
var quotedPath = $"\"{scriptPathWithSpaces.Replace("\"", "\\\"")}\"";
|
||||
|
||||
// Assert
|
||||
Assert.False(originalPath.StartsWith("\""), "Original path should not be quoted");
|
||||
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Fixed path should be properly quoted");
|
||||
Assert.Contains("path with spaces", quotedPath, StringComparison.Ordinal);
|
||||
|
||||
// Verify the path is properly quoted (platform-agnostic check)
|
||||
Assert.True(quotedPath.StartsWith("\"/path with spaces/_temp"), "Path should start with quoted temp directory");
|
||||
Assert.True(quotedPath.EndsWith("test-script.sh\""), "Path should end with quoted script name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ScriptPath_WithQuotes_ShouldEscapeQuotes()
|
||||
{
|
||||
// Arrange - Test paths that contain quotes
|
||||
var pathWithQuotes = "/path/\"quoted folder\"/script.sh";
|
||||
|
||||
// Test our fix - properly escape quotes and wrap in quotes
|
||||
var quotedPath = $"\"{pathWithQuotes.Replace("\"", "\\\"")}\"";
|
||||
|
||||
// Assert
|
||||
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Path should be wrapped in quotes");
|
||||
Assert.Contains("\\\"", quotedPath, StringComparison.Ordinal);
|
||||
Assert.Contains("quoted folder", quotedPath, StringComparison.Ordinal);
|
||||
|
||||
// Verify quotes are properly escaped
|
||||
Assert.Contains("\\\"quoted folder\\\"", quotedPath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ScriptPath_ActionsRunnerWithSpaces_ShouldBeQuoted()
|
||||
{
|
||||
// Arrange - Test the specific real-world scenario that was failing
|
||||
var runnerPathWithSpaces = "/Users/user/Downloads/actions-runner-osx-arm64-2.328.0 2";
|
||||
var tempPath = Path.Combine(runnerPathWithSpaces, "_work", "_temp");
|
||||
var scriptPath = Path.Combine(tempPath, "script-guid.sh");
|
||||
|
||||
// Test our fix
|
||||
var quotedPath = $"\"{scriptPath.Replace("\"", "\\\"")}\"";
|
||||
|
||||
// Assert
|
||||
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Path should be wrapped in quotes");
|
||||
Assert.Contains("actions-runner-osx-arm64-2.328.0 2", quotedPath, StringComparison.Ordinal);
|
||||
Assert.Contains("_work", quotedPath, StringComparison.Ordinal);
|
||||
Assert.Contains("_temp", quotedPath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ScriptPath_MultipleSpaces_ShouldBeQuoted()
|
||||
{
|
||||
// Arrange - Test paths with multiple spaces
|
||||
var pathWithMultipleSpaces = "/path/with multiple spaces/script.sh";
|
||||
|
||||
// Test our fix
|
||||
var quotedPath = $"\"{pathWithMultipleSpaces.Replace("\"", "\\\"")}\"";
|
||||
|
||||
// Assert
|
||||
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Path should be wrapped in quotes");
|
||||
Assert.Contains("multiple spaces", quotedPath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ScriptPath_WithoutSpaces_ShouldStillBeQuoted()
|
||||
{
|
||||
// Arrange - Test normal paths without spaces (regression test)
|
||||
var normalPath = "/home/user/runner/_work/_temp/script.sh";
|
||||
|
||||
// Test our fix
|
||||
var quotedPath = $"\"{normalPath.Replace("\"", "\\\"")}\"";
|
||||
|
||||
// Assert
|
||||
Assert.True(quotedPath.StartsWith("\"") && quotedPath.EndsWith("\""), "Path should be wrapped in quotes");
|
||||
Assert.Equal($"\"{normalPath}\"", quotedPath);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
[InlineData("/path with spaces/script.sh")]
|
||||
[InlineData("/Users/user/Downloads/actions-runner-osx-arm64-2.328.0 2/_work/_temp/guid.sh")]
|
||||
[InlineData("C:\\Program Files\\GitHub Runner\\script.cmd")]
|
||||
[InlineData("/path/\"with quotes\"/script.sh")]
|
||||
[InlineData("/path/with'single'quotes/script.sh")]
|
||||
public void ScriptPath_VariousScenarios_ShouldBeProperlyQuoted(string inputPath)
|
||||
{
|
||||
// Arrange & Act
|
||||
var quotedPath = $"\"{inputPath.Replace("\"", "\\\"")}\"";
|
||||
|
||||
// Assert
|
||||
Assert.True(quotedPath.StartsWith("\""), "Path should start with quote");
|
||||
Assert.True(quotedPath.EndsWith("\""), "Path should end with quote");
|
||||
|
||||
// Ensure the original path content is preserved
|
||||
var unquotedContent = quotedPath.Substring(1, quotedPath.Length - 2);
|
||||
if (inputPath.Contains("\""))
|
||||
{
|
||||
// If original had quotes, they should be escaped in the result
|
||||
Assert.Contains("\\\"", unquotedContent);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void FixUpScriptContents_BashEnvironmentVariables_ShouldQuoteRedirects()
|
||||
{
|
||||
// Arrange
|
||||
var scriptContent = @"echo ""## Dependency Status Report"" >> $GITHUB_STEP_SUMMARY
|
||||
echo ""Generated on: $(date)"" >> $GITHUB_STEP_SUMMARY
|
||||
echo ""| Component | Status |"" > $GITHUB_OUTPUT
|
||||
echo ""npm-status=ok"" >> $GITHUB_OUTPUT";
|
||||
|
||||
// Act
|
||||
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("bash", scriptContent);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(">> \"$GITHUB_STEP_SUMMARY\"", fixedContent);
|
||||
Assert.Contains("> \"$GITHUB_OUTPUT\"", fixedContent);
|
||||
Assert.DoesNotContain(">> $GITHUB_STEP_SUMMARY", fixedContent);
|
||||
Assert.DoesNotContain("> $GITHUB_OUTPUT", fixedContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void FixUpScriptContents_AlreadyQuotedVariables_ShouldNotDoubleQuote()
|
||||
{
|
||||
// Arrange
|
||||
var scriptContent = @"echo ""test"" >> ""$GITHUB_STEP_SUMMARY""
|
||||
echo ""test"" > '$GITHUB_OUTPUT'
|
||||
echo ""test"" 2>> ""$GITHUB_ENV""";
|
||||
|
||||
// Act
|
||||
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("bash", scriptContent);
|
||||
|
||||
// Assert - Should remain unchanged
|
||||
Assert.Equal(scriptContent, fixedContent);
|
||||
Assert.Contains(">> \"$GITHUB_STEP_SUMMARY\"", fixedContent);
|
||||
Assert.Contains("> '$GITHUB_OUTPUT'", fixedContent);
|
||||
Assert.Contains("2>> \"$GITHUB_ENV\"", fixedContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void FixUpScriptContents_ShellRedirectOperators_ShouldHandleAllTypes()
|
||||
{
|
||||
// Arrange
|
||||
var scriptContent = @"echo ""test"" >> $VAR1
|
||||
echo ""test"" > $VAR2
|
||||
cat < $VAR3
|
||||
echo ""test"" 2>> $VAR4
|
||||
echo ""test"" 2> $VAR5";
|
||||
|
||||
// Act
|
||||
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("sh", scriptContent);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(">> \"$VAR1\"", fixedContent);
|
||||
Assert.Contains("> \"$VAR2\"", fixedContent);
|
||||
Assert.Contains("< \"$VAR3\"", fixedContent);
|
||||
Assert.Contains("2>> \"$VAR4\"", fixedContent);
|
||||
Assert.Contains("2> \"$VAR5\"", fixedContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void FixUpScriptContents_NonShellTypes_ShouldNotModifyEnvironmentVariables()
|
||||
{
|
||||
// Arrange
|
||||
var scriptContent = @"echo ""test"" >> $GITHUB_STEP_SUMMARY";
|
||||
|
||||
// Act
|
||||
var powershellFixed = ScriptHandlerHelpers.FixUpScriptContents("powershell", scriptContent);
|
||||
var cmdFixed = ScriptHandlerHelpers.FixUpScriptContents("cmd", scriptContent);
|
||||
var pythonFixed = ScriptHandlerHelpers.FixUpScriptContents("python", scriptContent);
|
||||
|
||||
// Assert - Should not modify environment variables for non-shell types
|
||||
Assert.Contains(">> $GITHUB_STEP_SUMMARY", powershellFixed);
|
||||
Assert.Contains(">> $GITHUB_STEP_SUMMARY", cmdFixed);
|
||||
Assert.Contains(">> $GITHUB_STEP_SUMMARY", pythonFixed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void FixUpScriptContents_ComplexScript_ShouldQuoteOnlyUnquotedRedirects()
|
||||
{
|
||||
// Arrange
|
||||
var scriptContent = @"#!/bin/bash
|
||||
# This is a test script
|
||||
echo ""Starting workflow"" >> $GITHUB_STEP_SUMMARY
|
||||
echo ""Already quoted"" >> ""$GITHUB_OUTPUT""
|
||||
export MY_VAR=""$HOME/path with spaces""
|
||||
curl -s https://api.github.com/rate_limit > $TEMP_FILE
|
||||
echo ""Final status"" 2>> $ERROR_LOG
|
||||
if [ -f ""$GITHUB_ENV"" ]; then
|
||||
echo ""MY_VAR=test"" >> $GITHUB_ENV
|
||||
fi";
|
||||
|
||||
// Act
|
||||
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("bash", scriptContent);
|
||||
|
||||
// Assert
|
||||
Assert.Contains(">> \"$GITHUB_STEP_SUMMARY\"", fixedContent);
|
||||
Assert.Contains(">> \"$GITHUB_OUTPUT\"", fixedContent); // Should remain quoted
|
||||
Assert.Contains("> \"$TEMP_FILE\"", fixedContent);
|
||||
Assert.Contains("2>> \"$ERROR_LOG\"", fixedContent);
|
||||
Assert.Contains(">> \"$GITHUB_ENV\"", fixedContent);
|
||||
|
||||
// Other parts should remain unchanged
|
||||
Assert.Contains("#!/bin/bash", fixedContent);
|
||||
Assert.Contains("# This is a test script", fixedContent);
|
||||
Assert.Contains("export MY_VAR=\"$HOME/path with spaces\"", fixedContent);
|
||||
Assert.Contains("if [ -f \"$GITHUB_ENV\" ]; then", fixedContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void FixUpScriptContents_EnvironmentVariablesInCommands_ShouldNotQuote()
|
||||
{
|
||||
// Arrange - Environment variables not in redirects should not be touched
|
||||
var scriptContent = @"echo $GITHUB_STEP_SUMMARY
|
||||
cd $HOME
|
||||
ls -la $TEMP_DIR
|
||||
if [ ""$MY_VAR"" == ""test"" ]; then
|
||||
echo ""match""
|
||||
fi";
|
||||
|
||||
// Act
|
||||
var fixedContent = ScriptHandlerHelpers.FixUpScriptContents("bash", scriptContent);
|
||||
|
||||
// Assert - Should remain unchanged as these are not redirects
|
||||
Assert.Equal(scriptContent, fixedContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -567,5 +567,193 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task SnapshotPreflightChecks_HostedRunnerCheck_Enabled_GitHubHosted_Success()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
_jobEc.Global.Variables.Set(WellKnownDistributedTaskVariables.RunnerEnvironment, "github-hosted");
|
||||
|
||||
hc.SetSingleton<ISnapshotOperationProvider>(new SnapshotOperationProvider());
|
||||
_jobEc.Global.Variables.Set(Constants.Runner.Features.SnapshotPreflightHostedRunnerCheck, "true");
|
||||
|
||||
var jobExtension = new JobExtension();
|
||||
jobExtension.Initialize(hc);
|
||||
|
||||
|
||||
var snapshot = new Pipelines.Snapshot("TestImageNameForPreflightCheck");
|
||||
var imageNameValueStringToken = new StringToken(null, null, null, snapshot.ImageName);
|
||||
_message.Snapshot = imageNameValueStringToken;
|
||||
|
||||
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||
|
||||
await jobExtension.InitializeJob(_jobEc, _message);
|
||||
|
||||
var postJobSteps = _jobEc.PostJobSteps;
|
||||
Assert.Equal(1, postJobSteps.Count);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable("RUNNER_ENVIRONMENT", null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task SnapshotPreflightChecks_HostedRunnerCheck_Enabled_SelfHosted_ThrowsException()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
_jobEc.Global.Variables.Set(WellKnownDistributedTaskVariables.RunnerEnvironment, "self-hosted");
|
||||
hc.SetSingleton<ISnapshotOperationProvider>(new SnapshotOperationProvider());
|
||||
|
||||
var jobExtension = new JobExtension();
|
||||
jobExtension.Initialize(hc);
|
||||
|
||||
_jobEc.Global.Variables.Set(Constants.Runner.Features.SnapshotPreflightHostedRunnerCheck, "true");
|
||||
|
||||
var snapshot = new Pipelines.Snapshot("TestImageNameForPreflightCheck");
|
||||
var imageNameValueStringToken = new StringToken(null, null, null, snapshot.ImageName);
|
||||
_message.Snapshot = imageNameValueStringToken;
|
||||
|
||||
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(() => jobExtension.InitializeJob(_jobEc, _message));
|
||||
Assert.Contains("Snapshot workflows must be run on a GitHub Hosted Runner", exception.Message);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable("RUNNER_ENVIRONMENT", null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task SnapshotPreflightChecks_ImageGenPoolCheck_Enabled_ImageGenEnabled_Success()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", "true");
|
||||
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
hc.SetSingleton<ISnapshotOperationProvider>(new SnapshotOperationProvider());
|
||||
|
||||
var jobExtension = new JobExtension();
|
||||
jobExtension.Initialize(hc);
|
||||
|
||||
_jobEc.Global.Variables.Set(Constants.Runner.Features.SnapshotPreflightImageGenPoolCheck, "true");
|
||||
|
||||
var snapshot = new Pipelines.Snapshot("TestImageNameForPreflightCheck");
|
||||
var imageNameValueStringToken = new StringToken(null, null, null, snapshot.ImageName);
|
||||
_message.Snapshot = imageNameValueStringToken;
|
||||
|
||||
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||
|
||||
await jobExtension.InitializeJob(_jobEc, _message);
|
||||
|
||||
var postJobSteps = _jobEc.PostJobSteps;
|
||||
Assert.Equal(1, postJobSteps.Count);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task SnapshotPreflightChecks_ImageGenPoolCheck_Enabled_ImageGen_False_ThrowsException()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", "false");
|
||||
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
hc.SetSingleton<ISnapshotOperationProvider>(new SnapshotOperationProvider());
|
||||
_jobEc.SetRunnerContext("environment", "github-hosted");
|
||||
|
||||
var jobExtension = new JobExtension();
|
||||
jobExtension.Initialize(hc);
|
||||
|
||||
_jobEc.Global.Variables.Set(Constants.Runner.Features.SnapshotPreflightImageGenPoolCheck, "true");
|
||||
|
||||
var snapshot = new Pipelines.Snapshot("TestImageNameForPreflightCheck");
|
||||
var imageNameValueStringToken = new StringToken(null, null, null, snapshot.ImageName);
|
||||
_message.Snapshot = imageNameValueStringToken;
|
||||
|
||||
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(() => jobExtension.InitializeJob(_jobEc, _message));
|
||||
Assert.Contains("Snapshot workflows must be run a hosted runner with Image Generation enabled", exception.Message);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task SnapshotPreflightChecks_ImageGenPoolCheck_Enabled_ImageGen_Missing_ThrowsException()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
hc.SetSingleton<ISnapshotOperationProvider>(new SnapshotOperationProvider());
|
||||
|
||||
var jobExtension = new JobExtension();
|
||||
jobExtension.Initialize(hc);
|
||||
|
||||
_jobEc.Global.Variables.Set(Constants.Runner.Features.SnapshotPreflightImageGenPoolCheck, "true");
|
||||
|
||||
var snapshot = new Pipelines.Snapshot("TestImageNameForPreflightCheck");
|
||||
var imageNameValueStringToken = new StringToken(null, null, null, snapshot.ImageName);
|
||||
_message.Snapshot = imageNameValueStringToken;
|
||||
|
||||
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(() => jobExtension.InitializeJob(_jobEc, _message));
|
||||
Assert.Contains("Snapshot workflows must be run a hosted runner with Image Generation enabled", exception.Message);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task SnapshotPreflightChecks_BothChecks_Enabled_AllConditionsMet_Success()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", "true");
|
||||
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
hc.SetSingleton<ISnapshotOperationProvider>(new SnapshotOperationProvider());
|
||||
|
||||
var jobExtension = new JobExtension();
|
||||
jobExtension.Initialize(hc);
|
||||
|
||||
// Enable both preflight checks
|
||||
_jobEc.Global.Variables.Set(WellKnownDistributedTaskVariables.RunnerEnvironment, "github-hosted");
|
||||
_jobEc.Global.Variables.Set(Constants.Runner.Features.SnapshotPreflightHostedRunnerCheck, "true");
|
||||
_jobEc.Global.Variables.Set(Constants.Runner.Features.SnapshotPreflightImageGenPoolCheck, "true");
|
||||
|
||||
var snapshot = new Pipelines.Snapshot("TestImageNameForPreflightCheck");
|
||||
var imageNameValueStringToken = new StringToken(null, null, null, snapshot.ImageName);
|
||||
_message.Snapshot = imageNameValueStringToken;
|
||||
|
||||
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
|
||||
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
|
||||
|
||||
await jobExtension.InitializeJob(_jobEc, _message);
|
||||
|
||||
var postJobSteps = _jobEc.PostJobSteps;
|
||||
Assert.Equal(1, postJobSteps.Count);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable("RUNNER_ENVIRONMENT", null);
|
||||
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public class SnapshotOperationProviderL0
|
||||
Assert.NotNull(actualSnapshot);
|
||||
Assert.Equal(expectedSnapshot.ImageName, actualSnapshot!.ImageName);
|
||||
_ec.Verify(ec => ec.Write(null, $"Request written to: {_snapshotRequestFilePath}"), Times.Once);
|
||||
_ec.Verify(ec => ec.Write(null, $"Image Name: {expectedSnapshot.ImageName} Version: {expectedSnapshot.Version}"), Times.Once);
|
||||
_ec.Verify(ec => ec.Write(null, "This request will be processed after the job completes. You will not receive any feedback on the snapshot process within the workflow logs of this job."), Times.Once);
|
||||
_ec.Verify(ec => ec.Write(null, "If the snapshot process is successful, you should see a new image with the requested name in the list of available custom images when creating a new GitHub-hosted Runner."), Times.Once);
|
||||
_ec.VerifyNoOtherCalls();
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.328.0
|
||||
2.329.0
|
||||
|
||||
Reference in New Issue
Block a user