Compare commits

...

20 Commits

Author SHA1 Message Date
github-actions[bot]
3850703f46 chore: update Node versions (20: 20.20.1, 24: 24.14.0) 2026-03-09 06:25:01 +00:00
github-actions[bot]
2b98d42113 Update Docker to v29.3.0 and Buildx to v0.32.1 (#4286)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-09 00:37:54 +00:00
dependabot[bot]
ce8ce410b0 Bump @stylistic/eslint-plugin from 5.9.0 to 5.10.0 in /src/Misc/expressionFunc/hashFiles (#4281)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2026-03-07 22:13:23 +00:00
dependabot[bot]
5310e90af2 Bump actions/attest-build-provenance from 3 to 4 (#4266)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 01:59:22 +00:00
dependabot[bot]
98323280e8 Bump docker/setup-buildx-action from 3 to 4 (#4282)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2026-03-07 01:49:28 +00:00
dependabot[bot]
5ef3270368 Bump docker/build-push-action from 6 to 7 (#4283)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 01:44:58 +00:00
eric sciple
1138dd80f7 Fix positional arg bug in ExpressionParser.CreateTree (#4279) 2026-03-05 14:56:28 -06:00
dependabot[bot]
99910ca83e Bump docker/login-action from 3 to 4 (#4278)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2026-03-05 15:45:49 +00:00
dependabot[bot]
bcd04cfbf0 Bump actions/upload-artifact from 6 to 7 (#4270)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2026-03-05 14:55:48 +00:00
eric sciple
20111cbfda Support entrypoint and command for service containers (#4276) 2026-03-04 23:36:45 +00:00
Max Horstmann
8f01257663 Devcontainer: bump base image Ubuntu version (#4277) 2026-03-04 20:17:25 +00:00
eric sciple
8a73bccebb Fix parser comparison mismatches (#4273) 2026-03-03 05:38:16 +00:00
Tingluo Huang
a9a07a6553 Avoid throw in SelfUpdaters. (#4274) 2026-03-02 22:44:14 -05:00
github-actions[bot]
60a9422599 chore: update Node versions (#4272)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-02 13:51:11 +00:00
dependabot[bot]
985a06fcca Bump actions/download-artifact from 7 to 8 (#4269)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 09:18:13 +00:00
eric sciple
ae09a9d7b5 Fix composite post-step marker display names (#4267) 2026-02-26 08:36:55 -06:00
Tingluo Huang
7650fc432e Log inner exception message. (#4265) 2026-02-25 15:44:27 -05:00
Salman Chishti
bc00800857 Bump runner version to 2.332.0 and update release notes (#4264) 2026-02-25 13:36:47 +00:00
dependabot[bot]
86e23605d6 Bump @stylistic/eslint-plugin from 3.1.0 to 5.9.0 in /src/Misc/expressionFunc/hashFiles (#4257)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2026-02-25 12:02:23 +00:00
dependabot[bot]
0fb7482206 Bump minimatch in /src/Misc/expressionFunc/hashFiles (#4261)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 11:56:32 +00:00
38 changed files with 1093 additions and 176 deletions

View File

@@ -1,8 +1,8 @@
{ {
"name": "Actions Runner Devcontainer", "name": "Actions Runner Devcontainer",
"image": "mcr.microsoft.com/devcontainers/base:focal", "image": "mcr.microsoft.com/devcontainers/base:noble",
"features": { "features": {
"ghcr.io/devcontainers/features/docker-in-docker:1": {}, "ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/dotnet": { "ghcr.io/devcontainers/features/dotnet": {
"version": "8.0.418" "version": "8.0.418"
}, },

View File

@@ -78,7 +78,7 @@ jobs:
# Upload runner package tar.gz/zip as artifact # Upload runner package tar.gz/zip as artifact
- name: Publish Artifact - name: Publish Artifact
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: runner-package-${{ matrix.runtime }} name: runner-package-${{ matrix.runtime }}
path: | path: |
@@ -111,10 +111,10 @@ jobs:
core.setOutput('version', version); core.setOutput('version', version);
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: ./images context: ./images
load: true load: true

View File

@@ -38,10 +38,10 @@ jobs:
core.setOutput('version', runnerVersion); core.setOutput('version', runnerVersion);
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Log into registry ${{ env.REGISTRY }} - name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -49,7 +49,7 @@ jobs:
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: ./images context: ./images
platforms: | platforms: |
@@ -68,7 +68,7 @@ jobs:
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }} org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
- name: Generate attestation - name: Generate attestation
uses: actions/attest-build-provenance@v3 uses: actions/attest-build-provenance@v4
with: with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }} subject-digest: ${{ steps.build-and-push.outputs.digest }}

View File

@@ -118,7 +118,7 @@ jobs:
# Upload runner package tar.gz/zip as artifact. # Upload runner package tar.gz/zip as artifact.
- name: Publish Artifact - name: Publish Artifact
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: runner-packages-${{ matrix.runtime }} name: runner-packages-${{ matrix.runtime }}
path: | path: |
@@ -133,37 +133,37 @@ jobs:
# Download runner package tar.gz/zip produced by 'build' job # Download runner package tar.gz/zip produced by 'build' job
- name: Download Artifact (win-x64) - name: Download Artifact (win-x64)
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: runner-packages-win-x64 name: runner-packages-win-x64
path: ./ path: ./
- name: Download Artifact (win-arm64) - name: Download Artifact (win-arm64)
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: runner-packages-win-arm64 name: runner-packages-win-arm64
path: ./ path: ./
- name: Download Artifact (osx-x64) - name: Download Artifact (osx-x64)
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: runner-packages-osx-x64 name: runner-packages-osx-x64
path: ./ path: ./
- name: Download Artifact (osx-arm64) - name: Download Artifact (osx-arm64)
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: runner-packages-osx-arm64 name: runner-packages-osx-arm64
path: ./ path: ./
- name: Download Artifact (linux-x64) - name: Download Artifact (linux-x64)
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: runner-packages-linux-x64 name: runner-packages-linux-x64
path: ./ path: ./
- name: Download Artifact (linux-arm) - name: Download Artifact (linux-arm)
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: runner-packages-linux-arm name: runner-packages-linux-arm
path: ./ path: ./
- name: Download Artifact (linux-arm64) - name: Download Artifact (linux-arm64)
uses: actions/download-artifact@v7 uses: actions/download-artifact@v8
with: with:
name: runner-packages-linux-arm64 name: runner-packages-linux-arm64
path: ./ path: ./
@@ -309,10 +309,10 @@ jobs:
core.setOutput('version', runnerVersion); core.setOutput('version', runnerVersion);
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Log into registry ${{ env.REGISTRY }} - name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -320,7 +320,7 @@ jobs:
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: ./images context: ./images
platforms: | platforms: |
@@ -339,7 +339,7 @@ jobs:
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }} org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
- name: Generate attestation - name: Generate attestation
uses: actions/attest-build-provenance@v3 uses: actions/attest-build-provenance@v4
with: with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }} subject-digest: ${{ steps.build-and-push.outputs.digest }}

View File

@@ -5,8 +5,8 @@ ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
ARG RUNNER_VERSION ARG RUNNER_VERSION
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0 ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
ARG DOCKER_VERSION=29.2.0 ARG DOCKER_VERSION=29.3.0
ARG BUILDX_VERSION=0.31.1 ARG BUILDX_VERSION=0.32.1
RUN apt update -y && apt install curl unzip -y RUN apt update -y && apt install curl unzip -y

View File

@@ -1,27 +1,35 @@
## What's Changed ## What's Changed
* Fix owner of /home/runner directory by @nikola-jokic in https://github.com/actions/runner/pull/4132 * chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4200
* Update Docker to v29.0.2 and Buildx to v0.30.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4135 * Update dotnet sdk to latest version @8.0.417 by @github-actions[bot] in https://github.com/actions/runner/pull/4201
* Update workflow around runner docker image. by @TingluoHuang in https://github.com/actions/runner/pull/4133 * Bump System.Formats.Asn1 and System.Security.Cryptography.Pkcs by @dependabot[bot] in https://github.com/actions/runner/pull/4202
* Fix regex for validating runner version format by @TingluoHuang in https://github.com/actions/runner/pull/4136 * Allow empty container options by @ericsciple in https://github.com/actions/runner/pull/4208
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4144 * Update Docker to v29.1.5 and Buildx to v0.31.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4212
* Ensure safe_sleep tries alternative approaches by @TingluoHuang in https://github.com/actions/runner/pull/4146 * Report job level annotations by @TingluoHuang in https://github.com/actions/runner/pull/4216
* Bump actions/github-script from 7 to 8 by @dependabot[bot] in https://github.com/actions/runner/pull/4137 * Fix local action display name showing `Run /./` instead of `Run ./` by @ericsciple in https://github.com/actions/runner/pull/4218
* Bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4130 * Update Docker to v29.2.0 and Buildx to v0.31.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4219
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4149 * Add support for libssl3 and libssl3t64 for newer Debian/Ubuntu versions by @nekketsuuu in https://github.com/actions/runner/pull/4213
* Bump docker image to use ubuntu 24.04 by @TingluoHuang in https://github.com/actions/runner/pull/4018 * Validate work dir during runner start up. by @TingluoHuang in https://github.com/actions/runner/pull/4227
* Add support for case function by @AllanGuigou in https://github.com/actions/runner/pull/4147 * Bump hook to 0.8.1 by @nikola-jokic in https://github.com/actions/runner/pull/4222
* Cleanup feature flag actions_container_action_runner_temp by @ericsciple in https://github.com/actions/runner/pull/4163 * Support return job result as exitcode in hosted runner. by @TingluoHuang in https://github.com/actions/runner/pull/4233
* Bump actions/download-artifact from 6 to 7 by @dependabot[bot] in https://github.com/actions/runner/pull/4155 * Add telemetry tracking for deprecated set-output and save-state commands by @ericsciple in https://github.com/actions/runner/pull/4221
* Bump actions/upload-artifact from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4157 * Fix parser comparison mismatches by @ericsciple in https://github.com/actions/runner/pull/4220
* Set ACTIONS_ORCHESTRATION_ID as env to actions. by @TingluoHuang in https://github.com/actions/runner/pull/4178 * Remove unnecessary connection test during some registration flows by @zarenner in https://github.com/actions/runner/pull/4244
* Allow hosted VM report job telemetry via .setup_info file. by @TingluoHuang in https://github.com/actions/runner/pull/4186 * chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4249
* Bump typescript from 5.9.2 to 5.9.3 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4184 * Update dotnet sdk to latest version @8.0.418 by @github-actions[bot] in https://github.com/actions/runner/pull/4250
* Bump Azure.Storage.Blobs from 12.26.0 to 12.27.0 by @dependabot[bot] in https://github.com/actions/runner/pull/4189 * Fix link to SECURITY.md in README by @TingluoHuang in https://github.com/actions/runner/pull/4253
* Try to infer runner is on hosted/ghes when githuburl is empty. by @TingluoHuang in https://github.com/actions/runner/pull/4254
* Add Node.js 20 deprecation warning annotation (Phase 1) by @salmanmkc in https://github.com/actions/runner/pull/4242
* Update Node.js 20 deprecation date to June 2nd, 2026 by @salmanmkc in https://github.com/actions/runner/pull/4258
* Composite Action Step Markers by @ericsciple in https://github.com/actions/runner/pull/4243
* Symlink actions cache by @paveliak in https://github.com/actions/runner/pull/4260
* Bump minimatch in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4261
* Bump @stylistic/eslint-plugin from 3.1.0 to 5.9.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4257
## New Contributors ## New Contributors
* @AllanGuigou made their first contribution in https://github.com/actions/runner/pull/4147 * @nekketsuuu made their first contribution in https://github.com/actions/runner/pull/4213
* @zarenner made their first contribution in https://github.com/actions/runner/pull/4244
**Full Changelog**: https://github.com/actions/runner/compare/v2.330.0...v2.331.0 **Full Changelog**: https://github.com/actions/runner/compare/v2.331.0...v2.332.0
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet. _Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository. To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.

View File

@@ -12,7 +12,7 @@
"@actions/glob": "^0.4.0" "@actions/glob": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^3.1.0", "@stylistic/eslint-plugin": "^5.10.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
@@ -75,11 +75,10 @@
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0", "version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
@@ -229,23 +228,36 @@
} }
}, },
"node_modules/@stylistic/eslint-plugin": { "node_modules/@stylistic/eslint-plugin": {
"version": "3.1.0", "version": "5.10.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
"integrity": "sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==", "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/utils": "^8.13.0", "@eslint-community/eslint-utils": "^4.9.1",
"eslint-visitor-keys": "^4.2.0", "@typescript-eslint/types": "^8.56.0",
"espree": "^10.3.0", "eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"picomatch": "^4.0.2" "picomatch": "^4.0.3"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"eslint": ">=8.40.0" "eslint": "^9.0.0 || ^10.0.0"
}
},
"node_modules/@stylistic/eslint-plugin/node_modules/@typescript-eslint/types": {
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz",
"integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
@@ -524,24 +536,34 @@
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
"version": "2.0.2", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^5.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@@ -1769,23 +1791,34 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/eslint-plugin-github/node_modules/brace-expansion": { "node_modules/eslint-plugin-github/node_modules/balanced-match": {
"version": "2.0.2", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/eslint-plugin-github/node_modules/brace-expansion": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
} }
}, },
"node_modules/eslint-plugin-github/node_modules/minimatch": { "node_modules/eslint-plugin-github/node_modules/minimatch": {
"version": "9.0.4", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^5.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@@ -3300,9 +3333,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -4714,9 +4747,9 @@
} }
}, },
"@eslint-community/eslint-utils": { "@eslint-community/eslint-utils": {
"version": "4.9.0", "version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
@@ -4821,18 +4854,25 @@
} }
}, },
"@stylistic/eslint-plugin": { "@stylistic/eslint-plugin": {
"version": "3.1.0", "version": "5.10.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
"integrity": "sha512-pA6VOrOqk0+S8toJYhQGv2MWpQQR0QpeUo9AhNkC49Y26nxBQ/nH1rta9bUU1rPw2fJ1zZEMV5oCX5AazT7J2g==", "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/utils": "^8.13.0", "@eslint-community/eslint-utils": "^4.9.1",
"eslint-visitor-keys": "^4.2.0", "@typescript-eslint/types": "^8.56.0",
"espree": "^10.3.0", "eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"picomatch": "^4.0.2" "picomatch": "^4.0.3"
}, },
"dependencies": { "dependencies": {
"@typescript-eslint/types": {
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz",
"integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==",
"dev": true
},
"eslint-visitor-keys": { "eslint-visitor-keys": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
@@ -4992,22 +5032,28 @@
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
"dependencies": { "dependencies": {
"balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true
},
"brace-expansion": { "brace-expansion": {
"version": "2.0.2", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true, "dev": true,
"requires": { "requires": {
"balanced-match": "^1.0.0" "balanced-match": "^4.0.2"
} }
}, },
"minimatch": { "minimatch": {
"version": "9.0.5", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==",
"dev": true, "dev": true,
"requires": { "requires": {
"brace-expansion": "^2.0.1" "brace-expansion": "^5.0.2"
} }
}, },
"ts-api-utils": { "ts-api-utils": {
@@ -5831,22 +5877,28 @@
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
} }
}, },
"balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true
},
"brace-expansion": { "brace-expansion": {
"version": "2.0.2", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
"dev": true, "dev": true,
"requires": { "requires": {
"balanced-match": "^1.0.0" "balanced-match": "^4.0.2"
} }
}, },
"minimatch": { "minimatch": {
"version": "9.0.4", "version": "9.0.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==",
"dev": true, "dev": true,
"requires": { "requires": {
"brace-expansion": "^2.0.1" "brace-expansion": "^5.0.2"
} }
} }
} }
@@ -6883,9 +6935,9 @@
"dev": true "dev": true
}, },
"minimatch": { "minimatch": {
"version": "3.1.2", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }

View File

@@ -35,7 +35,7 @@
"@actions/glob": "^0.4.0" "@actions/glob": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^3.1.0", "@stylistic/eslint-plugin": "^5.10.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",

View File

@@ -6,8 +6,8 @@ NODE_URL=https://nodejs.org/dist
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version. # When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started # Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
NODE20_VERSION="20.20.0" NODE20_VERSION="20.20.1"
NODE24_VERSION="24.13.1" NODE24_VERSION="24.14.0"
get_abs_path() { get_abs_path() {
# exploits the fact that pwd will print abs path when no args # exploits the fact that pwd will print abs path when no args

View File

@@ -172,6 +172,7 @@ namespace GitHub.Runner.Common
public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_check"; public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_check";
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check"; public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser"; public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser";
public static readonly string ServiceContainerCommand = "actions_service_container_command";
public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions"; public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions";
public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations"; public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations";
public static readonly string EmitCompositeMarkers = "actions_runner_emit_composite_markers"; public static readonly string EmitCompositeMarkers = "actions_runner_emit_composite_markers";

View File

@@ -120,8 +120,10 @@ namespace GitHub.Runner.Listener
} }
catch (Exception ex) catch (Exception ex)
{ {
Trace.Error(ex);
_terminal.WriteError($"Runner update failed: {ex.Message}");
_updateTrace.Enqueue(ex.ToString()); _updateTrace.Enqueue(ex.ToString());
throw; return false;
} }
finally finally
{ {

View File

@@ -120,8 +120,10 @@ namespace GitHub.Runner.Listener
} }
catch (Exception ex) catch (Exception ex)
{ {
Trace.Error(ex);
_terminal.WriteError($"Runner update failed: {ex.Message}");
_updateTrace.Enqueue(ex.ToString()); _updateTrace.Enqueue(ex.ToString());
throw; return false;
} }
finally finally
{ {

View File

@@ -111,7 +111,7 @@ namespace GitHub.Runner.Worker
{ {
// Log the error and fail the PrepareActionsAsync Initialization. // Log the error and fail the PrepareActionsAsync Initialization.
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}"); Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
executionContext.InfrastructureError(ex.Message, category: "resolve_action"); executionContext.InfrastructureError(ex.InnerException?.Message ?? ex.Message, category: "resolve_action");
executionContext.Result = TaskResult.Failed; executionContext.Result = TaskResult.Failed;
throw; throw;
} }
@@ -818,7 +818,7 @@ namespace GitHub.Runner.Worker
try try
{ {
Trace.Info($"Found unpacked action directory '{cacheDirectory}' in cache directory '{actionArchiveCacheDir}'"); Trace.Info($"Found unpacked action directory '{cacheDirectory}' in cache directory '{actionArchiveCacheDir}'");
// repository archive from github always contains a nested folder // repository archive from github always contains a nested folder
var nestedDirectories = new DirectoryInfo(cacheDirectory).GetDirectories(); var nestedDirectories = new DirectoryInfo(cacheDirectory).GetDirectories();
if (nestedDirectories.Length != 1) if (nestedDirectories.Length != 1)
@@ -832,14 +832,14 @@ namespace GitHub.Runner.Worker
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken); IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
IOUtil.CreateSymbolicLink(destDirectory, nestedDirectories[0].FullName); IOUtil.CreateSymbolicLink(destDirectory, nestedDirectories[0].FullName);
} }
executionContext.Debug($"Created symlink from cached directory '{cacheDirectory}' to '{destDirectory}'"); executionContext.Debug($"Created symlink from cached directory '{cacheDirectory}' to '{destDirectory}'");
executionContext.Global.JobTelemetry.Add(new JobTelemetry() executionContext.Global.JobTelemetry.Add(new JobTelemetry()
{ {
Type = JobTelemetryType.General, Type = JobTelemetryType.General,
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache} via symlink" Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache} via symlink"
}); });
Trace.Info("Finished getting action repository."); Trace.Info("Finished getting action repository.");
return; return;
} }

View File

@@ -36,6 +36,8 @@ namespace GitHub.Runner.Worker.Container
this.ContainerImage = containerImage; this.ContainerImage = containerImage;
this.ContainerDisplayName = $"{container.Alias}_{Pipelines.Validation.NameValidation.Sanitize(containerImage)}_{Guid.NewGuid().ToString("N").Substring(0, 6)}"; this.ContainerDisplayName = $"{container.Alias}_{Pipelines.Validation.NameValidation.Sanitize(containerImage)}_{Guid.NewGuid().ToString("N").Substring(0, 6)}";
this.ContainerCreateOptions = container.Options; this.ContainerCreateOptions = container.Options;
this.ContainerEntryPoint = container.Entrypoint;
this.ContainerEntryPointArgs = container.Command;
_environmentVariables = container.Environment; _environmentVariables = container.Environment;
this.IsJobContainer = isJobContainer; this.IsJobContainer = isJobContainer;
this.ContainerNetworkAlias = networkAlias; this.ContainerNetworkAlias = networkAlias;

View File

@@ -1328,9 +1328,9 @@ namespace GitHub.Runner.Worker
UpdateGlobalStepsContext(); UpdateGlobalStepsContext();
} }
internal IPipelineTemplateEvaluator ToPipelineTemplateEvaluatorInternal(ObjectTemplating.ITraceWriter traceWriter = null) internal IPipelineTemplateEvaluator ToPipelineTemplateEvaluatorInternal(bool allowServiceContainerCommand, ObjectTemplating.ITraceWriter traceWriter = null)
{ {
return new PipelineTemplateEvaluatorWrapper(HostContext, this, traceWriter); return new PipelineTemplateEvaluatorWrapper(HostContext, this, allowServiceContainerCommand, traceWriter);
} }
private static void NoOp() private static void NoOp()
@@ -1418,10 +1418,13 @@ namespace GitHub.Runner.Worker
public static IPipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null) public static IPipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null)
{ {
var allowServiceContainerCommand = (context.Global.Variables.GetBoolean(Constants.Runner.Features.ServiceContainerCommand) ?? false)
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_SERVICE_CONTAINER_COMMAND"));
// Create wrapper? // Create wrapper?
if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER"))) if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER")))
{ {
return (context as ExecutionContext).ToPipelineTemplateEvaluatorInternal(traceWriter); return (context as ExecutionContext).ToPipelineTemplateEvaluatorInternal(allowServiceContainerCommand, traceWriter);
} }
// Legacy // Legacy
@@ -1433,6 +1436,7 @@ namespace GitHub.Runner.Worker
return new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable) return new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable)
{ {
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
AllowServiceContainerCommand = allowServiceContainerCommand,
}; };
} }

View File

@@ -312,7 +312,14 @@ namespace GitHub.Runner.Worker.Handlers
// Emit start marker after full context setup so display name expressions resolve correctly // Emit start marker after full context setup so display name expressions resolve correctly
if (emitCompositeMarkers) if (emitCompositeMarkers)
{ {
step.TryUpdateDisplayName(out _); try
{
step.EvaluateDisplayName(step.ExecutionContext.ExpressionValues, step.ExecutionContext, out _);
}
catch (Exception ex)
{
Trace.Warning("Caught exception while evaluating embedded step display name. {0}", ex);
}
ExecutionContext.Output($"##[start-action display={EscapeProperty(SanitizeDisplayName(step.DisplayName))};id={EscapeProperty(markerId)}]"); ExecutionContext.Output($"##[start-action display={EscapeProperty(SanitizeDisplayName(step.DisplayName))};id={EscapeProperty(markerId)}]");
stepStopwatch = Stopwatch.StartNew(); stepStopwatch = Stopwatch.StartNew();
} }

View File

@@ -23,6 +23,7 @@ namespace GitHub.Runner.Worker
public PipelineTemplateEvaluatorWrapper( public PipelineTemplateEvaluatorWrapper(
IHostContext hostContext, IHostContext hostContext,
IExecutionContext context, IExecutionContext context,
bool allowServiceContainerCommand,
ObjectTemplating.ITraceWriter traceWriter = null) ObjectTemplating.ITraceWriter traceWriter = null)
{ {
ArgUtil.NotNull(hostContext, nameof(hostContext)); ArgUtil.NotNull(hostContext, nameof(hostContext));
@@ -40,11 +41,14 @@ namespace GitHub.Runner.Worker
_legacyEvaluator = new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable) _legacyEvaluator = new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable)
{ {
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
AllowServiceContainerCommand = allowServiceContainerCommand,
}; };
// New evaluator // New evaluator
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter(); var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
_newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, context.Global.FileTable, features: null) var features = WorkflowFeatures.GetDefaults();
features.AllowServiceContainerCommand = allowServiceContainerCommand;
_newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, context.Global.FileTable, features)
{ {
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
}; };
@@ -401,6 +405,18 @@ namespace GitHub.Runner.Worker
return false; return false;
} }
if (!string.Equals(legacyResult.Entrypoint, newResult.Entrypoint, StringComparison.Ordinal))
{
_trace.Info($"CompareJobContainer mismatch - Entrypoint differs (legacy='{legacyResult.Entrypoint}', new='{newResult.Entrypoint}')");
return false;
}
if (!string.Equals(legacyResult.Command, newResult.Command, StringComparison.Ordinal))
{
_trace.Info($"CompareJobContainer mismatch - Command differs (legacy='{legacyResult.Command}', new='{newResult.Command}')");
return false;
}
if (!CompareDictionaries(legacyResult.Environment, newResult.Environment, "Environment")) if (!CompareDictionaries(legacyResult.Environment, newResult.Environment, "Environment"))
{ {
return false; return false;

View File

@@ -20,7 +20,7 @@ namespace GitHub.DistributedTask.Expressions2
IEnumerable<IFunctionInfo> functions, IEnumerable<IFunctionInfo> functions,
Boolean allowCaseFunction = true) Boolean allowCaseFunction = true)
{ {
var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction); var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction: allowCaseFunction);
context.Trace.Info($"Parsing expression: <{expression}>"); context.Trace.Info($"Parsing expression: <{expression}>");
return CreateTree(context); return CreateTree(context);
} }

View File

@@ -39,6 +39,24 @@ namespace GitHub.DistributedTask.Pipelines
set; set;
} }
/// <summary>
/// Gets or sets the container entrypoint override.
/// </summary>
public String Entrypoint
{
get;
set;
}
/// <summary>
/// Gets or sets the container command and args (after the image name).
/// </summary>
public String Command
{
get;
set;
}
/// <summary> /// <summary>
/// Gets or sets the volumes which are mounted into the container. /// Gets or sets the volumes which are mounted into the container.
/// </summary> /// </summary>

View File

@@ -47,6 +47,8 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
public const String NumberStrategyContext = "number-strategy-context"; public const String NumberStrategyContext = "number-strategy-context";
public const String On = "on"; public const String On = "on";
public const String Options = "options"; public const String Options = "options";
public const String Entrypoint = "entrypoint";
public const String Command = "command";
public const String Outputs = "outputs"; public const String Outputs = "outputs";
public const String OutputsPattern = "needs.*.outputs"; public const String OutputsPattern = "needs.*.outputs";
public const String Password = "password"; public const String Password = "password";

View File

@@ -237,7 +237,8 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
internal static JobContainer ConvertToJobContainer( internal static JobContainer ConvertToJobContainer(
TemplateContext context, TemplateContext context,
TemplateToken value, TemplateToken value,
bool allowExpressions = false) bool allowExpressions = false,
bool allowServiceContainerCommand = false)
{ {
var result = new JobContainer(); var result = new JobContainer();
if (allowExpressions && value.Traverse().Any(x => x is ExpressionToken)) if (allowExpressions && value.Traverse().Any(x => x is ExpressionToken))
@@ -280,6 +281,22 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
case PipelineTemplateConstants.Options: case PipelineTemplateConstants.Options:
result.Options = containerPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Container} {propertyName}").Value; result.Options = containerPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Container} {propertyName}").Value;
break; break;
case PipelineTemplateConstants.Entrypoint:
if (!allowServiceContainerCommand)
{
context.Error(containerPropertyPair.Key, $"The key '{PipelineTemplateConstants.Entrypoint}' is not allowed");
break;
}
result.Entrypoint = containerPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Container} {propertyName}").Value;
break;
case PipelineTemplateConstants.Command:
if (!allowServiceContainerCommand)
{
context.Error(containerPropertyPair.Key, $"The key '{PipelineTemplateConstants.Command}' is not allowed");
break;
}
result.Command = containerPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Container} {propertyName}").Value;
break;
case PipelineTemplateConstants.Ports: case PipelineTemplateConstants.Ports:
var ports = containerPropertyPair.Value.AssertSequence($"{PipelineTemplateConstants.Container} {propertyName}"); var ports = containerPropertyPair.Value.AssertSequence($"{PipelineTemplateConstants.Container} {propertyName}");
var portList = new List<String>(ports.Count); var portList = new List<String>(ports.Count);
@@ -326,7 +343,8 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
internal static List<KeyValuePair<String, JobContainer>> ConvertToJobServiceContainers( internal static List<KeyValuePair<String, JobContainer>> ConvertToJobServiceContainers(
TemplateContext context, TemplateContext context,
TemplateToken services, TemplateToken services,
bool allowExpressions = false) bool allowExpressions = false,
bool allowServiceContainerCommand = false)
{ {
var result = new List<KeyValuePair<String, JobContainer>>(); var result = new List<KeyValuePair<String, JobContainer>>();
@@ -340,7 +358,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
foreach (var servicePair in servicesMapping) foreach (var servicePair in servicesMapping)
{ {
var networkAlias = servicePair.Key.AssertString("services key").Value; var networkAlias = servicePair.Key.AssertString("services key").Value;
var container = ConvertToJobContainer(context, servicePair.Value); var container = ConvertToJobContainer(context, servicePair.Value, allowExpressions, allowServiceContainerCommand);
result.Add(new KeyValuePair<String, JobContainer>(networkAlias, container)); result.Add(new KeyValuePair<String, JobContainer>(networkAlias, container));
} }

View File

@@ -51,6 +51,8 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
public Int32 MaxResultSize { get; set; } = 10 * 1024 * 1024; // 10 mb public Int32 MaxResultSize { get; set; } = 10 * 1024 * 1024; // 10 mb
public bool AllowServiceContainerCommand { get; set; }
public Boolean EvaluateStepContinueOnError( public Boolean EvaluateStepContinueOnError(
TemplateToken token, TemplateToken token,
DictionaryContextData contextData, DictionaryContextData contextData,
@@ -357,7 +359,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
{ {
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Services, token, 0, null, omitHeader: true); token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Services, token, 0, null, omitHeader: true);
context.Errors.Check(); context.Errors.Check();
result = PipelineTemplateConverter.ConvertToJobServiceContainers(context, token); result = PipelineTemplateConverter.ConvertToJobServiceContainers(context, token, allowServiceContainerCommand: AllowServiceContainerCommand);
} }
catch (Exception ex) when (!(ex is TemplateValidationException)) catch (Exception ex) when (!(ex is TemplateValidationException))
{ {

View File

@@ -430,6 +430,21 @@
} }
}, },
"service-container-mapping": {
"mapping": {
"properties": {
"image": "string",
"options": "string",
"entrypoint": "string",
"command": "string",
"env": "container-env",
"ports": "sequence-of-non-empty-string",
"volumes": "sequence-of-non-empty-string",
"credentials": "container-registry-credentials"
}
}
},
"services": { "services": {
"context": [ "context": [
"github", "github",
@@ -454,7 +469,7 @@
], ],
"one-of": [ "one-of": [
"string", "string",
"container-mapping" "service-container-mapping"
] ]
}, },

View File

@@ -62,6 +62,8 @@ namespace GitHub.Actions.WorkflowParser.Conversion
public const String NumberStrategyContext = "number-strategy-context"; public const String NumberStrategyContext = "number-strategy-context";
public const String On = "on"; public const String On = "on";
public const String Options = "options"; public const String Options = "options";
public const String Entrypoint = "entrypoint";
public const String Command = "command";
public const String Org = "org"; public const String Org = "org";
public const String Organization = "organization"; public const String Organization = "organization";
public const String Outputs = "outputs"; public const String Outputs = "outputs";

View File

@@ -1079,7 +1079,8 @@ namespace GitHub.Actions.WorkflowParser.Conversion
internal static JobContainer ConvertToJobContainer( internal static JobContainer ConvertToJobContainer(
TemplateContext context, TemplateContext context,
TemplateToken value, TemplateToken value,
bool isEarlyValidation = false) bool isEarlyValidation = false,
bool isServiceContainer = false)
{ {
var result = new JobContainer(); var result = new JobContainer();
if (isEarlyValidation && value.Traverse().Any(x => x is ExpressionToken)) if (isEarlyValidation && value.Traverse().Any(x => x is ExpressionToken))
@@ -1089,11 +1090,34 @@ namespace GitHub.Actions.WorkflowParser.Conversion
if (value is StringToken containerLiteral) if (value is StringToken containerLiteral)
{ {
if (String.IsNullOrEmpty(containerLiteral.Value)) // Trim "docker://"
var trimmedImage = containerLiteral.Value;
var hasDockerPrefix = containerLiteral.Value.StartsWith(WorkflowTemplateConstants.DockerUriPrefix, StringComparison.Ordinal);
if (hasDockerPrefix)
{ {
trimmedImage = trimmedImage.Substring(WorkflowTemplateConstants.DockerUriPrefix.Length);
}
// Empty shorthand after trimming "docker://" ?
if (String.IsNullOrEmpty(trimmedImage))
{
// Error at parse-time for:
// 1. container: 'docker://'
// 2. services.foo: ''
// 3. services.foo: 'docker://'
//
// Do not error for:
// 1. container: ''
if (isEarlyValidation && (hasDockerPrefix || isServiceContainer))
{
context.Error(value, "Container image cannot be empty");
}
// Short-circuit
return null; return null;
} }
// Store original, trimmed further below
result.Image = containerLiteral.Value; result.Image = containerLiteral.Value;
} }
else else
@@ -1122,6 +1146,22 @@ namespace GitHub.Actions.WorkflowParser.Conversion
case WorkflowTemplateConstants.Options: case WorkflowTemplateConstants.Options:
result.Options = containerPropertyPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName}").Value; result.Options = containerPropertyPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName}").Value;
break; break;
case WorkflowTemplateConstants.Entrypoint:
if (!context.GetFeatures().AllowServiceContainerCommand)
{
context.Error(containerPropertyPair.Key, $"The key '{WorkflowTemplateConstants.Entrypoint}' is not allowed");
break;
}
result.Entrypoint = containerPropertyPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName}").Value;
break;
case WorkflowTemplateConstants.Command:
if (!context.GetFeatures().AllowServiceContainerCommand)
{
context.Error(containerPropertyPair.Key, $"The key '{WorkflowTemplateConstants.Command}' is not allowed");
break;
}
result.Command = containerPropertyPair.Value.AssertString($"{WorkflowTemplateConstants.Container} {propertyName}").Value;
break;
case WorkflowTemplateConstants.Ports: case WorkflowTemplateConstants.Ports:
var ports = containerPropertyPair.Value.AssertSequence($"{WorkflowTemplateConstants.Container} {propertyName}"); var ports = containerPropertyPair.Value.AssertSequence($"{WorkflowTemplateConstants.Container} {propertyName}");
var portList = new List<String>(ports.Count); var portList = new List<String>(ports.Count);
@@ -1152,22 +1192,30 @@ namespace GitHub.Actions.WorkflowParser.Conversion
} }
} }
// Trim "docker://"
var hadDockerPrefix = false;
if (!String.IsNullOrEmpty(result.Image) && result.Image.StartsWith(WorkflowTemplateConstants.DockerUriPrefix, StringComparison.Ordinal))
{
hadDockerPrefix = true;
result.Image = result.Image.Substring(WorkflowTemplateConstants.DockerUriPrefix.Length);
}
if (String.IsNullOrEmpty(result.Image)) if (String.IsNullOrEmpty(result.Image))
{ {
// Only error during early validation (parse time) // Error at parse-time for:
// At runtime (expression evaluation), empty image = no container // 1. container: {image: 'docker://'}
if (isEarlyValidation) // 2. services.foo: {image: ''}
// 3. services.foo: {image: 'docker://'}
//
// Do not error for:
// 1. container: {image: ''}
if (isEarlyValidation && (hadDockerPrefix || isServiceContainer))
{ {
context.Error(value, "Container image cannot be empty"); context.Error(value, "Container image cannot be empty");
} }
return null; return null;
} }
if (result.Image.StartsWith(WorkflowTemplateConstants.DockerUriPrefix, StringComparison.Ordinal))
{
result.Image = result.Image.Substring(WorkflowTemplateConstants.DockerUriPrefix.Length);
}
return result; return result;
} }
@@ -1188,7 +1236,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion
foreach (var servicePair in servicesMapping) foreach (var servicePair in servicesMapping)
{ {
var networkAlias = servicePair.Key.AssertString("services key").Value; var networkAlias = servicePair.Key.AssertString("services key").Value;
var container = ConvertToJobContainer(context, servicePair.Value); var container = ConvertToJobContainer(context, servicePair.Value, isEarlyValidation, isServiceContainer: true);
result.Add(new KeyValuePair<String, JobContainer>(networkAlias, container)); result.Add(new KeyValuePair<String, JobContainer>(networkAlias, container));
} }

View File

@@ -35,6 +35,24 @@ namespace GitHub.Actions.WorkflowParser
set; set;
} }
/// <summary>
/// Gets or sets the container entrypoint override.
/// </summary>
public String Entrypoint
{
get;
set;
}
/// <summary>
/// Gets or sets the container command and args (after the image name).
/// </summary>
public String Command
{
get;
set;
}
/// <summary> /// <summary>
/// Gets or sets the volumes which are mounted into the container. /// Gets or sets the volumes which are mounted into the container.
/// </summary> /// </summary>

View File

@@ -48,6 +48,13 @@ namespace GitHub.Actions.WorkflowParser
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public bool StrictJsonParsing { get; set; } public bool StrictJsonParsing { get; set; }
/// <summary>
/// Gets or sets a value indicating whether service containers may specify "entrypoint" and "command".
/// Used during parsing and evaluation.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public bool AllowServiceContainerCommand { get; set; }
/// <summary> /// <summary>
/// Gets the default workflow features. /// Gets the default workflow features.
/// </summary> /// </summary>
@@ -60,6 +67,7 @@ namespace GitHub.Actions.WorkflowParser
Snapshot = false, // Default to false since this feature is still in an experimental phase Snapshot = false, // Default to false since this feature is still in an experimental phase
StrictJsonParsing = false, // Default to false since this is temporary for telemetry purposes only StrictJsonParsing = false, // Default to false since this is temporary for telemetry purposes only
AllowModelsPermission = false, // Default to false since we want this to be disabled for all non-production environments AllowModelsPermission = false, // Default to false since we want this to be disabled for all non-production environments
AllowServiceContainerCommand = false, // Default to false since this feature is gated by actions_service_container_command
}; };
} }

View File

@@ -2589,21 +2589,53 @@
"mapping": { "mapping": {
"properties": { "properties": {
"image": { "image": {
"type": "non-empty-string", "type": "string",
"description": "Use `jobs.<job_id>.container.image` to define the Docker image to use as the container to run the action. The value can be the Docker Hub image or a registry name." "description": "The Docker image to use as the container. The value can be the Docker Hub image or a registry name."
}, },
"options": { "options": {
"type": "string", "type": "string",
"description": "Use `jobs.<job_id>.container.options` to configure additional Docker container resource options." "description": "Additional Docker container resource options."
}, },
"env": "container-env", "env": "container-env",
"ports": { "ports": {
"type": "sequence-of-non-empty-string", "type": "sequence-of-non-empty-string",
"description": "Use `jobs.<job_id>.container.ports` to set an array of ports to expose on the container." "description": "An array of ports to expose on the container."
}, },
"volumes": { "volumes": {
"type": "sequence-of-non-empty-string", "type": "sequence-of-non-empty-string",
"description": "Use `jobs.<job_id>.container.volumes` to set an array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host." "description": "An array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host."
},
"credentials": "container-registry-credentials"
}
}
},
"service-container-mapping": {
"mapping": {
"properties": {
"image": {
"type": "string",
"description": "The Docker image to use as the container. The value can be the Docker Hub image or a registry name."
},
"options": {
"type": "string",
"description": "Additional Docker container resource options."
},
"entrypoint": {
"type": "string",
"description": "Override the default ENTRYPOINT in the service container image."
},
"command": {
"type": "string",
"description": "Override the default CMD in the service container image."
},
"env": "container-env",
"ports": {
"type": "sequence-of-non-empty-string",
"description": "An array of ports to expose on the container."
},
"volumes": {
"type": "sequence-of-non-empty-string",
"description": "An array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host."
}, },
"credentials": "container-registry-credentials" "credentials": "container-registry-credentials"
} }
@@ -2634,12 +2666,12 @@
"matrix" "matrix"
], ],
"one-of": [ "one-of": [
"non-empty-string", "string",
"container-mapping" "service-container-mapping"
] ]
}, },
"container-registry-credentials": { "container-registry-credentials": {
"description": "If the image's container registry requires authentication to pull the image, you can use `jobs.<job_id>.container.credentials` to set a map of the username and password. The credentials are the same values that you would provide to the `docker login` command.", "description": "If the container registry requires authentication to pull the image, set a map of the username and password. The credentials are the same values that you would provide to the `docker login` command.",
"context": [ "context": [
"github", "github",
"inputs", "inputs",
@@ -2655,7 +2687,7 @@
} }
}, },
"container-env": { "container-env": {
"description": "Use `jobs.<job_id>.container.env` to set a map of variables in the container.", "description": "A map of environment variables to set in the container.",
"mapping": { "mapping": {
"loose-key-type": "non-empty-string", "loose-key-type": "non-empty-string",
"loose-value-type": "string-runner-context" "loose-value-type": "string-runner-context"

View File

@@ -228,8 +228,8 @@ namespace GitHub.Runner.Common.Tests.Listener
.Returns(Task.FromResult(new TaskAgent())); .Returns(Task.FromResult(new TaskAgent()));
var ex = await Assert.ThrowsAsync<TaskCanceledException>(() => updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken)); var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
Assert.Contains($"failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts", ex.Message); Assert.False(result);
} }
} }
finally finally
@@ -281,8 +281,8 @@ namespace GitHub.Runner.Common.Tests.Listener
.Returns(Task.FromResult(new TaskAgent())); .Returns(Task.FromResult(new TaskAgent()));
var ex = await Assert.ThrowsAsync<Exception>(() => updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken)); var result = await updater.SelfUpdate(_refreshMessage, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
Assert.Contains("did not match expected Runner Hash", ex.Message); Assert.False(result);
} }
} }
finally finally

View File

@@ -170,8 +170,8 @@ namespace GitHub.Runner.Common.Tests.Listener
DownloadUrl = "https://github.com/actions/runner/notexists" DownloadUrl = "https://github.com/actions/runner/notexists"
}; };
var ex = await Assert.ThrowsAsync<TaskCanceledException>(() => updater.SelfUpdate(message, _jobDispatcher.Object, true, hc.RunnerShutdownToken)); var result = await updater.SelfUpdate(message, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
Assert.Contains($"failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts", ex.Message); Assert.False(result);
} }
} }
finally finally
@@ -220,8 +220,8 @@ namespace GitHub.Runner.Common.Tests.Listener
SHA256Checksum = "badhash" SHA256Checksum = "badhash"
}; };
var ex = await Assert.ThrowsAsync<Exception>(() => updater.SelfUpdate(message, _jobDispatcher.Object, true, hc.RunnerShutdownToken)); var result = await updater.SelfUpdate(message, _jobDispatcher.Object, true, hc.RunnerShutdownToken);
Assert.Contains("did not match expected Runner Hash", ex.Message); Assert.False(result);
} }
} }
finally finally

View File

@@ -0,0 +1,104 @@
using GitHub.DistributedTask.Expressions2;
using GitHub.DistributedTask.Expressions2.Sdk;
using GitHub.DistributedTask.ObjectTemplating;
using System;
using System.Collections.Generic;
using Xunit;
namespace GitHub.Runner.Common.Tests.Sdk
{
/// <summary>
/// Regression tests for ExpressionParser.CreateTree to verify that
/// allowCaseFunction does not accidentally set allowUnknownKeywords.
/// </summary>
public sealed class ExpressionParserL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void CreateTree_RejectsUnrecognizedNamedValue()
{
// Regression: allowCaseFunction was passed positionally into
// the allowUnknownKeywords parameter, causing all named values
// to be silently accepted.
var parser = new ExpressionParser();
var namedValues = new List<INamedValueInfo>
{
new NamedValueInfo<ContextValueNode>("inputs"),
};
var ex = Assert.Throws<ParseException>(() =>
parser.CreateTree("github.event.repository.private", null, namedValues, null));
Assert.Contains("Unrecognized named-value", ex.Message);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void CreateTree_AcceptsRecognizedNamedValue()
{
var parser = new ExpressionParser();
var namedValues = new List<INamedValueInfo>
{
new NamedValueInfo<ContextValueNode>("inputs"),
};
var node = parser.CreateTree("inputs.foo", null, namedValues, null);
Assert.NotNull(node);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void CreateTree_CaseFunctionWorks_WhenAllowed()
{
var parser = new ExpressionParser();
var namedValues = new List<INamedValueInfo>
{
new NamedValueInfo<ContextValueNode>("github"),
};
var node = parser.CreateTree("case(github.event_name, 'push', 'Push Event')", null, namedValues, null, allowCaseFunction: true);
Assert.NotNull(node);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void CreateTree_CaseFunctionRejected_WhenDisallowed()
{
var parser = new ExpressionParser();
var namedValues = new List<INamedValueInfo>
{
new NamedValueInfo<ContextValueNode>("github"),
};
var ex = Assert.Throws<ParseException>(() =>
parser.CreateTree("case(github.event_name, 'push', 'Push Event')", null, namedValues, null, allowCaseFunction: false));
Assert.Contains("Unrecognized function", ex.Message);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void CreateTree_CaseFunctionDoesNotAffectUnknownKeywords()
{
// The key regression test: with allowCaseFunction=true (default),
// unrecognized named values must still be rejected.
var parser = new ExpressionParser();
var namedValues = new List<INamedValueInfo>
{
new NamedValueInfo<ContextValueNode>("inputs"),
};
var ex = Assert.Throws<ParseException>(() =>
parser.CreateTree("github.ref", null, namedValues, null, allowCaseFunction: true));
Assert.Contains("Unrecognized named-value", ex.Message);
}
}
}

View File

@@ -928,6 +928,58 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Load_ContainerAction_RejectsInvalidExpressionContext()
{
try
{
// Arrange
Setup();
var actionManifest = new ActionManifestManager();
actionManifest.Initialize(_hc);
// Act & Assert — github is not a valid context for container-runs-env (only inputs is allowed)
var ex = Assert.Throws<ArgumentException>(() =>
actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_env_invalid_context.yml")));
Assert.Contains("Failed to load", ex.Message);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Load_ContainerAction_AcceptsValidExpressionContext()
{
try
{
// Arrange
Setup();
var actionManifest = new ActionManifestManager();
actionManifest.Initialize(_hc);
// Act — inputs is a valid context for container-runs-env
var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_arg_env_expression.yml"));
// Assert
var containerAction = result.Execution as ContainerActionExecutionDataNew;
Assert.NotNull(containerAction);
Assert.Equal("${{ inputs.entryPoint }}", containerAction.Environment[1].Value.ToString());
}
finally
{
Teardown();
}
}
private void Setup([CallerMemberName] string name = "") private void Setup([CallerMemberName] string name = "")
{ {
_ecTokenSource?.Dispose(); _ecTokenSource?.Dispose();

View File

@@ -926,6 +926,58 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Load_ContainerAction_RejectsInvalidExpressionContext()
{
try
{
// Arrange
Setup();
var actionManifest = new ActionManifestManagerLegacy();
actionManifest.Initialize(_hc);
// Act & Assert — github is not a valid context for container-runs-env (only inputs is allowed)
var ex = Assert.Throws<ArgumentException>(() =>
actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_env_invalid_context.yml")));
Assert.Contains("Failed to load", ex.Message);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Load_ContainerAction_AcceptsValidExpressionContext()
{
try
{
// Arrange
Setup();
var actionManifest = new ActionManifestManagerLegacy();
actionManifest.Initialize(_hc);
// Act — inputs is a valid context for container-runs-env
var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_arg_env_expression.yml"));
// Assert
var containerAction = result.Execution as ContainerActionExecutionData;
Assert.NotNull(containerAction);
Assert.Equal("${{ inputs.entryPoint }}", containerAction.Environment[1].Value.ToString());
}
finally
{
Teardown();
}
}
private void Setup([CallerMemberName] string name = "") private void Setup([CallerMemberName] string name = "")
{ {
_ecTokenSource?.Dispose(); _ecTokenSource?.Dispose();

View File

@@ -379,6 +379,40 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Load_BothParsersRejectInvalidExpressionContext()
{
try
{
// Arrange — regression test: both parsers must reject github context
// in container-runs-env (only inputs is allowed per schema)
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var legacyManager = new ActionManifestManagerLegacy();
legacyManager.Initialize(_hc);
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
var newManager = new ActionManifestManager();
newManager.Initialize(_hc);
_hc.SetSingleton<IActionManifestManager>(newManager);
var wrapper = new ActionManifestManagerWrapper();
wrapper.Initialize(_hc);
var manifestPath = Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_env_invalid_context.yml");
// Act & Assert — both parsers should reject, wrapper should throw
Assert.Throws<ArgumentException>(() => wrapper.Load(_ec.Object, manifestPath));
}
finally
{
Teardown();
}
}
private string GetFullExceptionMessage(Exception ex) private string GetFullExceptionMessage(Exception ex)
{ {
var messages = new List<string>(); var messages = new List<string>();

View File

@@ -1,14 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi; using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util; using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using GitHub.Runner.Worker; using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Handlers; using GitHub.Runner.Worker.Handlers;
using Moq; using Moq;
using Newtonsoft.Json.Linq;
using Xunit; using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi; using DTWebApi = GitHub.DistributedTask.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Common.Tests.Worker.Handlers namespace GitHub.Runner.Common.Tests.Worker.Handlers
{ {
@@ -250,6 +254,66 @@ namespace GitHub.Runner.Common.Tests.Worker.Handlers
Assert.Equal("##[end-action id=failing-step;outcome=failure;conclusion=success;duration_ms=500]", marker); Assert.Equal("##[end-action id=failing-step;outcome=failure;conclusion=success;duration_ms=500]", marker);
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void PostStepMarker_UsesEvaluatedDisplayName()
{
// Arrange: create an ActionRunner with a RepositoryPathReference (simulating actions/cache@v4)
// and Stage = Post. Verify that EvaluateDisplayName produces the correct display name
// so the composite marker emits "Run actions/cache@v4" instead of the fallback "run".
var hc = new TestHostContext(this, nameof(PostStepMarker_UsesEvaluatedDisplayName));
var actionManifestLegacy = new ActionManifestManagerLegacy();
actionManifestLegacy.Initialize(hc);
hc.SetSingleton<IActionManifestManagerLegacy>(actionManifestLegacy);
var actionManifestNew = new ActionManifestManager();
actionManifestNew.Initialize(hc);
hc.SetSingleton<IActionManifestManager>(actionManifestNew);
var actionManifestManager = new ActionManifestManagerWrapper();
actionManifestManager.Initialize(hc);
hc.SetSingleton<IActionManifestManagerWrapper>(actionManifestManager);
var ec = new Mock<IExecutionContext>();
var contextData = new DictionaryContextData();
var githubContext = new GitHubContext();
githubContext.Add("event", JToken.Parse("{\"foo\":\"bar\"}").ToPipelineContextData());
contextData.Add("github", githubContext);
#if OS_WINDOWS
contextData["env"] = new DictionaryContextData();
#else
contextData["env"] = new CaseSensitiveDictionaryContextData();
#endif
ec.Setup(x => x.Global).Returns(new GlobalContext());
ec.Setup(x => x.ExpressionValues).Returns(contextData);
ec.Setup(x => x.ExpressionFunctions).Returns(new List<GitHub.DistributedTask.Expressions2.IFunctionInfo>());
ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()));
ec.Object.Global.Variables = new Variables(hc, new Dictionary<string, VariableValue>());
var actionRunner = new ActionRunner();
actionRunner.Initialize(hc);
actionRunner.ExecutionContext = ec.Object;
actionRunner.Stage = ActionRunStage.Post;
actionRunner.Action = new Pipelines.ActionStep()
{
Name = "cache",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
Name = "actions/cache",
Ref = "v4"
}
};
// Act: call EvaluateDisplayName directly, which is what CompositeActionHandler now does
// for embedded steps (including Post stage) instead of TryUpdateDisplayName.
var result = actionRunner.EvaluateDisplayName(contextData, ec.Object, out bool updated);
// Assert: display name should be "Run actions/cache@v4", not the fallback "run"
Assert.True(result);
Assert.True(updated);
Assert.Equal("Run actions/cache@v4", actionRunner.DisplayName);
}
// Helper methods that call the real production code // Helper methods that call the real production code
private static string EscapeProperty(string value) => private static string EscapeProperty(string value) =>
CompositeActionHandler.EscapeProperty(value); CompositeActionHandler.EscapeProperty(value);

View File

@@ -36,7 +36,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new StringToken(null, null, null, "test-value"); var token = new StringToken(null, null, null, "test-value");
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
@@ -63,7 +63,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
// Call EvaluateAndCompare directly: the new evaluator cancels the token // Call EvaluateAndCompare directly: the new evaluator cancels the token
// and returns a different value, forcing hasMismatch = true. // and returns a different value, forcing hasMismatch = true.
@@ -98,7 +98,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
// Different results without cancellation — mismatch SHOULD be recorded. // Different results without cancellation — mismatch SHOULD be recorded.
var result = wrapper.EvaluateAndCompare<string, string>( var result = wrapper.EvaluateAndCompare<string, string>(
@@ -130,7 +130,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new BooleanToken(null, null, null, true); var token = new BooleanToken(null, null, null, true);
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>(); var functions = new List<LegacyExpressions.IFunctionInfo>();
@@ -156,7 +156,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new MappingToken(null, null, null); var token = new MappingToken(null, null, null);
token.Add(new StringToken(null, null, null, "FOO"), new StringToken(null, null, null, "bar")); token.Add(new StringToken(null, null, null, "FOO"), new StringToken(null, null, null, "bar"));
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
@@ -184,7 +184,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new BasicExpressionToken(null, null, null, "true"); var token = new BasicExpressionToken(null, null, null, "true");
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>(); var functions = new List<LegacyExpressions.IFunctionInfo>();
@@ -211,7 +211,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new MappingToken(null, null, null); var token = new MappingToken(null, null, null);
token.Add(new StringToken(null, null, null, "input1"), new StringToken(null, null, null, "val1")); token.Add(new StringToken(null, null, null, "input1"), new StringToken(null, null, null, "val1"));
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
@@ -239,7 +239,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new NumberToken(null, null, null, 10); var token = new NumberToken(null, null, null, 10);
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>(); var functions = new List<LegacyExpressions.IFunctionInfo>();
@@ -265,7 +265,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new StringToken(null, null, null, ""); var token = new StringToken(null, null, null, "");
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>(); var functions = new List<LegacyExpressions.IFunctionInfo>();
@@ -281,6 +281,140 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobContainer_DockerPrefixOnly_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new StringToken(null, null, null, "docker://");
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
Assert.Null(result);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobContainer_DockerPrefixOnlyMapping_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new MappingToken(null, null, null);
token.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "docker://"));
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
Assert.Null(result);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobContainer_EmptyImageMapping_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new MappingToken(null, null, null);
token.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, ""));
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
Assert.Null(result);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobContainer_ValidImage_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new StringToken(null, null, null, "ubuntu:latest");
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
Assert.NotNull(result);
Assert.Equal("ubuntu:latest", result.Image);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobContainer_DockerPrefixWithImage_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new StringToken(null, null, null, "docker://ubuntu:latest");
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
Assert.NotNull(result);
Assert.Equal("ubuntu:latest", result.Image);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
@@ -291,7 +425,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new MappingToken(null, null, null); var token = new MappingToken(null, null, null);
token.Add(new StringToken(null, null, null, "out1"), new StringToken(null, null, null, "val1")); token.Add(new StringToken(null, null, null, "out1"), new StringToken(null, null, null, "val1"));
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
@@ -319,7 +453,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new StringToken(null, null, null, "https://example.com"); var token = new StringToken(null, null, null, "https://example.com");
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>(); var functions = new List<LegacyExpressions.IFunctionInfo>();
@@ -348,7 +482,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var token = new MappingToken(null, null, null); var token = new MappingToken(null, null, null);
token.Add(new StringToken(null, null, null, "shell"), new StringToken(null, null, null, "bash")); token.Add(new StringToken(null, null, null, "shell"), new StringToken(null, null, null, "bash"));
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
@@ -376,7 +510,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>(); var functions = new List<LegacyExpressions.IFunctionInfo>();
@@ -391,6 +525,213 @@ namespace GitHub.Runner.Common.Tests.Worker
} }
} }
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobServiceContainers_EmptyImage_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
// Build a services mapping token with one service whose image is empty string
// Similar to: services: { db: { image: '' } }
var servicesMapping = new MappingToken(null, null, null);
var serviceMapping = new MappingToken(null, null, null);
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, ""));
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
// Should get a list with one entry where the container is null (empty image = no container)
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("db", result[0].Key);
Assert.Null(result[0].Value);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobServiceContainers_DockerPrefixOnlyImage_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var servicesMapping = new MappingToken(null, null, null);
var serviceMapping = new MappingToken(null, null, null);
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "docker://"));
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("db", result[0].Key);
Assert.Null(result[0].Value);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobServiceContainers_ExpressionEvalsToEmpty_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
// Simulates: services: { db: { image: ${{ condition && 'img' || '' }} } }
// where the expression evaluates to '' at runtime
var servicesMapping = new MappingToken(null, null, null);
var serviceMapping = new MappingToken(null, null, null);
serviceMapping.Add(new StringToken(null, null, null, "image"), new BasicExpressionToken(null, null, null, "''"));
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("db", result[0].Key);
Assert.Null(result[0].Value);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobServiceContainers_ValidImage_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var servicesMapping = new MappingToken(null, null, null);
var serviceMapping = new MappingToken(null, null, null);
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("db", result[0].Key);
Assert.NotNull(result[0].Value);
Assert.Equal("postgres:latest", result[0].Value.Image);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobServiceContainers_EntrypointAndCommand_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var servicesMapping = new MappingToken(null, null, null);
var serviceMapping = new MappingToken(null, null, null);
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
serviceMapping.Add(new StringToken(null, null, null, "entrypoint"), new StringToken(null, null, null, "/bin/bash"));
serviceMapping.Add(new StringToken(null, null, null, "command"), new StringToken(null, null, null, "-lc echo hi"));
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: true);
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal("db", result[0].Key);
Assert.NotNull(result[0].Value);
Assert.Equal("postgres:latest", result[0].Value.Image);
Assert.Equal("/bin/bash", result[0].Value.Entrypoint);
Assert.Equal("-lc echo hi", result[0].Value.Command);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobServiceContainers_EntrypointAndCommand_FlagOff_BothParsersAgree()
{
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var servicesMapping = new MappingToken(null, null, null);
var serviceMapping = new MappingToken(null, null, null);
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
serviceMapping.Add(new StringToken(null, null, null, "entrypoint"), new StringToken(null, null, null, "/bin/bash"));
serviceMapping.Add(new StringToken(null, null, null, "command"), new StringToken(null, null, null, "-lc echo hi"));
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>();
Assert.Throws<GitHub.DistributedTask.ObjectTemplating.TemplateValidationException>(() =>
wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions));
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
@@ -401,7 +742,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
var contextData = new DictionaryContextData(); var contextData = new DictionaryContextData();
var functions = new List<LegacyExpressions.IFunctionInfo>(); var functions = new List<LegacyExpressions.IFunctionInfo>();
@@ -430,7 +771,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
// Both throw JsonReaderException with different messages — should be treated as equivalent // Both throw JsonReaderException with different messages — should be treated as equivalent
var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken from JsonReader. Path '', line 0, position 0."); var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken from JsonReader. Path '', line 0, position 0.");
@@ -461,7 +802,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
// Legacy throws Newtonsoft JsonReaderException, new throws System.Text.Json.JsonException // Legacy throws Newtonsoft JsonReaderException, new throws System.Text.Json.JsonException
var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken"); var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken");
@@ -492,7 +833,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Setup(); Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true"); _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object); var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
// Both throw non-JSON exceptions with different messages — should record mismatch // Both throw non-JSON exceptions with different messages — should record mismatch
var legacyEx = new InvalidOperationException("some error"); var legacyEx = new InvalidOperationException("some error");

View File

@@ -0,0 +1,13 @@
name: 'Action With Invalid Context'
description: 'Docker action that uses github context in env (only inputs is allowed)'
inputs:
my-input:
description: 'A test input'
required: false
default: 'hello'
runs:
using: 'docker'
image: 'Dockerfile'
env:
VALID: '${{ inputs.my-input }}'
INVALID: '${{ github.event.repository.private }}'

View File

@@ -1 +1 @@
2.331.0 2.332.0