Compare commits

...

28 Commits

Author SHA1 Message Date
eric sciple
90da9fbb8c Add CutoverWorkflowParser feature flag for workflow parser cutover
Add a new feature flag (actions_runner_cutover_workflow_parser) that enables
the wrapper classes to use only the new workflow parser/evaluator implementation
while converting results back to legacy types for callers.

Flag precedence: cutover > compare > legacy-only.
Rename EvaluateAndCompare to EvaluateWrapper in both wrapper classes.
2026-02-06 16:17:00 +00:00
github-actions[bot]
3ffedabea3 Update Docker to v29.2.0 and Buildx to v0.31.1 (#4219)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-02 02:15:37 +00:00
eric sciple
3a80a78cae Fix local action display name showing Run /./ instead of Run ./ (#4218) 2026-01-30 09:24:06 -06:00
Tingluo Huang
6822f4aba2 Report job level annotations (#4216) 2026-01-27 16:52:25 -05:00
github-actions[bot]
ad43c639cf Update Docker to v29.1.5 and Buildx to v0.31.0 (#4212)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-25 21:10:56 -05:00
eric sciple
5d4fb30d5b Allow empty container options (#4208) 2026-01-22 15:17:18 -06:00
dependabot[bot]
1df72a54ca Bump System.Formats.Asn1 and System.Security.Cryptography.Pkcs (#4202)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 14:41:15 +00:00
github-actions[bot]
02013cf967 Update dotnet sdk to latest version @8.0.417 (#4201)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-19 23:08:47 -05:00
github-actions[bot]
7d5c17a190 chore: update Node versions (#4200)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-20 02:18:53 +00:00
Allan Guigou
3f43560cb9 Prepare runner release 2.331.0 (#4190) 2026-01-09 12:15:39 -05:00
dependabot[bot]
73f7dbb681 Bump Azure.Storage.Blobs from 12.26.0 to 12.27.0 (#4189)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-09 14:54:40 +00:00
dependabot[bot]
f554a6446d Bump typescript from 5.9.2 to 5.9.3 in /src/Misc/expressionFunc/hashFiles (#4184)
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-01-07 18:52:44 +00:00
Tingluo Huang
bdceac4ab3 Allow hosted VM report job telemetry via .setup_info file. (#4186) 2026-01-07 13:27:22 -05:00
Tingluo Huang
3f1dd45172 Set ACTIONS_ORCHESTRATION_ID as env to actions. (#4178)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TingluoHuang <1750815+TingluoHuang@users.noreply.github.com>
2026-01-06 14:06:47 -05:00
dependabot[bot]
cf8f50b4d8 Bump actions/upload-artifact from 5 to 6 (#4157)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2025-12-21 08:31:15 +00:00
dependabot[bot]
2cf22c4858 Bump actions/download-artifact from 6 to 7 (#4155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2025-12-18 23:52:35 +00:00
eric sciple
04d77df0c7 Cleanup feature flag actions_container_action_runner_temp (#4163) 2025-12-18 14:53:43 -06:00
Allan Guigou
651077689d Add support for case function (#4147)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-17 15:57:05 +00:00
Tingluo Huang
c96dcd4729 Bump docker image to use ubuntu 24.04 (#4018) 2025-12-12 13:38:45 -05:00
github-actions[bot]
4b0058f15c chore: update Node versions (#4149)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-12 14:57:21 +00:00
dependabot[bot]
87d1dfb798 Bump actions/checkout from 5 to 6 (#4130)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2025-12-12 11:00:47 +00:00
dependabot[bot]
c992a2b406 Bump actions/github-script from 7 to 8 (#4137)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Salman Chishti <salmanmkc@GitHub.com>
2025-12-12 10:54:38 +00:00
Tingluo Huang
b2204f1fab Ensure safe_sleep tries alternative approaches (#4146) 2025-12-11 09:53:50 -05:00
github-actions[bot]
f99c3e6ee8 chore: update Node versions (#4144)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-08 16:52:16 +00:00
Tingluo Huang
463496e4fb Fix regex for validating runner version format (#4136) 2025-11-24 10:30:33 -05:00
Tingluo Huang
3f9f6f3994 Update workflow around runner docker image. (#4133) 2025-11-24 08:59:01 -05:00
github-actions[bot]
221f65874f Update Docker to v29.0.2 and Buildx to v0.30.1 (#4135)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-24 11:37:28 +00:00
Nikola Jokic
9a21440691 Fix owner of /home/runner directory (#4132) 2025-11-21 16:15:17 -05:00
54 changed files with 1057 additions and 136 deletions

View File

@@ -4,7 +4,7 @@
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
"ghcr.io/devcontainers/features/dotnet": {
"version": "8.0.416"
"version": "8.0.417"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"

View File

@@ -14,6 +14,9 @@ on:
paths-ignore:
- '**.md'
permissions:
contents: read
jobs:
build:
strategy:
@@ -50,7 +53,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# Build runner layout
- name: Build & Layout Release
@@ -75,8 +78,53 @@ jobs:
# Upload runner package tar.gz/zip as artifact
- name: Publish Artifact
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: runner-package-${{ matrix.runtime }}
path: |
_package
docker:
strategy:
matrix:
os: [ ubuntu-latest, ubuntu-24.04-arm ]
include:
- os: ubuntu-latest
docker_platform: linux/amd64
- os: ubuntu-24.04-arm
docker_platform: linux/arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- name: Get latest runner version
id: latest_runner
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const release = await github.rest.repos.getLatestRelease({
owner: 'actions',
repo: 'runner',
});
const version = release.data.tag_name.replace(/^v/, '');
core.setOutput('version', version);
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: ./images
load: true
platforms: ${{ matrix.docker_platform }}
tags: |
${{ github.sha }}:latest
build-args: |
RUNNER_VERSION=${{ steps.latest_runner.outputs.version }}
- name: Test Docker image
run: |
docker run --rm ${{ github.sha }}:latest ./run.sh --version

View File

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

View File

@@ -29,7 +29,7 @@ jobs:
npm-vulnerabilities: ${{ steps.check-versions.outputs.npm-vulnerabilities }}
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:

View File

@@ -17,7 +17,7 @@ jobs:
BUILDX_CURRENT_VERSION: ${{ steps.check_buildx_version.outputs.CURRENT_VERSION }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Check Docker version
id: check_docker_version
@@ -89,7 +89,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Update Docker version
shell: bash

75
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Publish DockerImage from Release Branch
on:
workflow_dispatch:
inputs:
releaseBranch:
description: 'Release Branch (releases/mXXX)'
required: true
jobs:
publish-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.releaseBranch }}
- name: Compute image version
id: image
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const runnerVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '');
console.log(`Using runner version ${runnerVersion}`);
if (!/^\d+\.\d+\.\d+$/.test(runnerVersion)) {
throw new Error(`Invalid runner version: ${runnerVersion}`);
}
core.setOutput('version', runnerVersion);
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
context: ./images
platforms: |
linux/amd64
linux/arm64
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
build-args: |
RUNNER_VERSION=${{ steps.image.outputs.version }}
push: true
labels: |
org.opencontainers.image.source=${{github.server_url}}/${{github.repository}}
org.opencontainers.image.licenses=MIT
annotations: |
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
- name: Generate attestation
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-and-push.outputs.digest }}
push-to-registry: true

View File

@@ -15,7 +15,7 @@ jobs:
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get current major minor version
id: fetch_current_version
shell: bash
@@ -89,7 +89,7 @@ jobs:
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
- name: Create Pull Request

View File

@@ -9,7 +9,7 @@ jobs:
update-node:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get latest Node versions
id: node-versions
run: |

View File

@@ -7,7 +7,7 @@ jobs:
npm-audit-with-ts-fix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:

View File

@@ -9,7 +9,7 @@ jobs:
npm-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6

View File

@@ -11,12 +11,12 @@ jobs:
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# Make sure ./releaseVersion match ./src/runnerversion
# Query GitHub release ensure version is not used
- name: Check version
uses: actions/github-script@v8.0.0
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -86,7 +86,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# Build runner layout
- name: Build & Layout Release
@@ -118,7 +118,7 @@ jobs:
# Upload runner package tar.gz/zip as artifact.
- name: Publish Artifact
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: runner-packages-${{ matrix.runtime }}
path: |
@@ -129,41 +129,41 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# Download runner package tar.gz/zip produced by 'build' job
- name: Download Artifact (win-x64)
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: runner-packages-win-x64
path: ./
- name: Download Artifact (win-arm64)
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: runner-packages-win-arm64
path: ./
- name: Download Artifact (osx-x64)
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: runner-packages-osx-x64
path: ./
- name: Download Artifact (osx-arm64)
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: runner-packages-osx-arm64
path: ./
- name: Download Artifact (linux-x64)
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: runner-packages-linux-x64
path: ./
- name: Download Artifact (linux-arm)
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: runner-packages-linux-arm
path: ./
- name: Download Artifact (linux-arm64)
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: runner-packages-linux-arm64
path: ./
@@ -171,7 +171,7 @@ jobs:
# Create ReleaseNote file
- name: Create ReleaseNote
id: releaseNote
uses: actions/github-script@v8.0.0
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
@@ -296,11 +296,11 @@ jobs:
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Compute image version
id: image
uses: actions/github-script@v8.0.0
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
@@ -334,8 +334,9 @@ jobs:
push: true
labels: |
org.opencontainers.image.source=${{github.server_url}}/${{github.repository}}
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
org.opencontainers.image.licenses=MIT
annotations: |
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
- name: Generate attestation
uses: actions/attest-build-provenance@v3

View File

@@ -1,12 +1,12 @@
# Source: https://github.com/dotnet/dotnet-docker
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy AS build
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-noble AS build
ARG TARGETOS
ARG TARGETARCH
ARG RUNNER_VERSION
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
ARG DOCKER_VERSION=29.0.1
ARG BUILDX_VERSION=0.30.0
ARG DOCKER_VERSION=29.2.0
ARG BUILDX_VERSION=0.31.1
RUN apt update -y && apt install curl unzip -y
@@ -33,15 +33,15 @@ RUN export RUNNER_ARCH=${TARGETARCH} \
&& rm -rf docker.tgz \
&& mkdir -p /usr/local/lib/docker/cli-plugins \
&& curl -fLo /usr/local/lib/docker/cli-plugins/docker-buildx \
"https://github.com/docker/buildx/releases/download/v${BUILDX_VERSION}/buildx-v${BUILDX_VERSION}.linux-${TARGETARCH}" \
"https://github.com/docker/buildx/releases/download/v${BUILDX_VERSION}/buildx-v${BUILDX_VERSION}.linux-${TARGETARCH}" \
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-noble
ENV DEBIAN_FRONTEND=noninteractive
ENV RUNNER_MANUALLY_TRAP_SIG=1
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
ENV ImageOS=ubuntu22
ENV ImageOS=ubuntu24
# 'gpg-agent' and 'software-properties-common' are needed for the 'add-apt-repository' command that follows
RUN apt update -y \
@@ -54,8 +54,6 @@ RUN add-apt-repository ppa:git-core/ppa \
&& apt install -y git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /home/runner
RUN adduser --disabled-password --gecos "" --uid 1001 runner \
&& groupadd docker --gid 123 \
&& usermod -aG sudo runner \
@@ -64,6 +62,8 @@ RUN adduser --disabled-password --gecos "" --uid 1001 runner \
&& echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers \
&& chmod 777 /home/runner
WORKDIR /home/runner
COPY --chown=runner:docker --from=build /actions-runner .
COPY --from=build /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-buildx

View File

@@ -1,30 +1,27 @@
## What's Changed
* Custom Image: Preflight checks by @lawrencegripper in https://github.com/actions/runner/pull/4081
* Update dotnet sdk to latest version @8.0.415 by @github-actions[bot] in https://github.com/actions/runner/pull/4080
* Link to an extant discussion category by @jsoref in https://github.com/actions/runner/pull/4084
* Improve logic around decide IsHostedServer. by @TingluoHuang in https://github.com/actions/runner/pull/4086
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4093
* Compare updated template evaluator by @ericsciple in https://github.com/actions/runner/pull/4092
* fix(dockerfile): set more lenient permissions on /home/runner by @caxu-rh in https://github.com/actions/runner/pull/4083
* Add support for libicu73-76 for newer Debian/Ubuntu versions by @lets-build-an-ocean in https://github.com/actions/runner/pull/4098
* Bump actions/download-artifact from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4089
* Bump actions/upload-artifact from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/4088
* Bump Azure.Storage.Blobs from 12.25.1 to 12.26.0 by @dependabot[bot] in https://github.com/actions/runner/pull/4077
* Only start runner after network is online by @dupondje in https://github.com/actions/runner/pull/4094
* Retry http error related to DNS resolution failure. by @TingluoHuang in https://github.com/actions/runner/pull/4110
* Update Docker to v29.0.1 and Buildx to v0.30.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4114
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4115
* Update dotnet sdk to latest version @8.0.416 by @github-actions[bot] in https://github.com/actions/runner/pull/4116
* Compare updated workflow parser for ActionManifestManager by @ericsciple in https://github.com/actions/runner/pull/4111
* Bump npm pkg version for hashFiles. by @TingluoHuang in https://github.com/actions/runner/pull/4122
* Fix owner of /home/runner directory by @nikola-jokic in https://github.com/actions/runner/pull/4132
* 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 workflow around runner docker image. by @TingluoHuang in https://github.com/actions/runner/pull/4133
* Fix regex for validating runner version format by @TingluoHuang in https://github.com/actions/runner/pull/4136
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4144
* Ensure safe_sleep tries alternative approaches by @TingluoHuang in https://github.com/actions/runner/pull/4146
* Bump actions/github-script from 7 to 8 by @dependabot[bot] in https://github.com/actions/runner/pull/4137
* Bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4130
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4149
* Bump docker image to use ubuntu 24.04 by @TingluoHuang in https://github.com/actions/runner/pull/4018
* Add support for case function by @AllanGuigou in https://github.com/actions/runner/pull/4147
* Cleanup feature flag actions_container_action_runner_temp by @ericsciple in https://github.com/actions/runner/pull/4163
* Bump actions/download-artifact from 6 to 7 by @dependabot[bot] in https://github.com/actions/runner/pull/4155
* Bump actions/upload-artifact from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4157
* Set ACTIONS_ORCHESTRATION_ID as env to actions. by @TingluoHuang in https://github.com/actions/runner/pull/4178
* Allow hosted VM report job telemetry via .setup_info file. by @TingluoHuang in https://github.com/actions/runner/pull/4186
* 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
* Bump Azure.Storage.Blobs from 12.26.0 to 12.27.0 by @dependabot[bot] in https://github.com/actions/runner/pull/4189
## New Contributors
* @lawrencegripper made their first contribution in https://github.com/actions/runner/pull/4081
* @caxu-rh made their first contribution in https://github.com/actions/runner/pull/4083
* @lets-build-an-ocean made their first contribution in https://github.com/actions/runner/pull/4098
* @dupondje made their first contribution in https://github.com/actions/runner/pull/4094
* @AllanGuigou made their first contribution in https://github.com/actions/runner/pull/4147
**Full Changelog**: https://github.com/actions/runner/compare/v2.329.0...v2.330.0
**Full Changelog**: https://github.com/actions/runner/compare/v2.330.0...v2.331.0
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.

View File

@@ -23,7 +23,7 @@
"husky": "^9.1.7",
"lint-staged": "^15.5.0",
"prettier": "^3.0.3",
"typescript": "^5.9.2"
"typescript": "^5.9.3"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -4439,9 +4439,9 @@
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -7643,9 +7643,9 @@
}
},
"typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true
},
"unbox-primitive": {

View File

@@ -46,6 +46,6 @@
"husky": "^9.1.7",
"lint-staged": "^15.5.0",
"prettier": "^3.0.3",
"typescript": "^5.9.2"
"typescript": "^5.9.3"
}
}

View File

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

View File

@@ -1,5 +1,36 @@
#!/bin/bash
# try to use sleep if available
if [ -x "$(command -v sleep)" ]; then
sleep "$1"
exit 0
fi
# try to use ping if available
if [ -x "$(command -v ping)" ]; then
ping -c $(( $1 + 1 )) 127.0.0.1 > /dev/null
exit 0
fi
# try to use read -t from stdin/stdout/stderr if we are in bash
if [ -n "$BASH_VERSION" ]; then
if command -v read >/dev/null 2>&1; then
if [ -t 0 ]; then
read -t "$1" -u 0 || :;
exit 0
fi
if [ -t 1 ]; then
read -t "$1" -u 1 || :;
exit 0
fi
if [ -t 2 ]; then
read -t "$1" -u 2 || :;
exit 0
fi
fi
fi
# fallback to a busy wait
SECONDS=0
while [[ $SECONDS -lt $1 ]]; do
:

View File

@@ -169,23 +169,25 @@ namespace GitHub.Runner.Common
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors";
public static readonly string ContainerActionRunnerTemp = "actions_container_action_runner_temp";
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 CompareWorkflowParser = "actions_runner_compare_workflow_parser";
public static readonly string CutoverWorkflowParser = "actions_runner_cutover_workflow_parser";
public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions";
public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations";
}
// Node version migration related constants
public static class NodeMigration
{
// Node versions
public static readonly string Node20 = "node20";
public static readonly string Node24 = "node24";
// Environment variables for controlling node version selection
public static readonly string ForceNode24Variable = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24";
public static readonly string AllowUnsecureNodeVersionVariable = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
// Feature flags for controlling the migration phases
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";

View File

@@ -316,6 +316,7 @@ namespace GitHub.Runner.Worker
Schema = _actionManifestSchema,
// TODO: Switch to real tracewriter for cutover
TraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter(),
AllowCaseFunction = false,
};
// Expression values from execution context

View File

@@ -315,6 +315,7 @@ namespace GitHub.Runner.Worker
maxBytes: 10 * 1024 * 1024),
Schema = _actionManifestSchema,
TraceWriter = executionContext.ToTemplateTraceWriter(),
AllowCaseFunction = false,
};
// Expression values from execution context

View File

@@ -40,7 +40,7 @@ namespace GitHub.Runner.Worker
public ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile)
{
return EvaluateAndCompare(
return EvaluateWrapper(
executionContext,
"Load",
() => _legacyManager.Load(executionContext, manifestFile),
@@ -53,7 +53,7 @@ namespace GitHub.Runner.Worker
TemplateToken token,
IDictionary<string, PipelineContextData> extraExpressionValues)
{
return EvaluateAndCompare(
return EvaluateWrapper(
executionContext,
"EvaluateCompositeOutputs",
() => _legacyManager.EvaluateCompositeOutputs(executionContext, token, extraExpressionValues),
@@ -66,7 +66,7 @@ namespace GitHub.Runner.Worker
SequenceToken token,
IDictionary<string, PipelineContextData> extraExpressionValues)
{
return EvaluateAndCompare(
return EvaluateWrapper(
executionContext,
"EvaluateContainerArguments",
() => _legacyManager.EvaluateContainerArguments(executionContext, token, extraExpressionValues),
@@ -79,12 +79,13 @@ namespace GitHub.Runner.Worker
MappingToken token,
IDictionary<string, PipelineContextData> extraExpressionValues)
{
return EvaluateAndCompare(
return EvaluateWrapper(
executionContext,
"EvaluateContainerEnvironment",
() => _legacyManager.EvaluateContainerEnvironment(executionContext, token, extraExpressionValues),
() => _newManager.EvaluateContainerEnvironment(executionContext, ConvertToNewToken(token) as GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.MappingToken, ConvertToNewExpressionValues(extraExpressionValues)),
(legacyResult, newResult) => {
(legacyResult, newResult) =>
{
var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper));
return CompareDictionaries(trace, legacyResult, newResult, "ContainerEnvironment");
});
@@ -95,7 +96,7 @@ namespace GitHub.Runner.Worker
string inputName,
TemplateToken token)
{
return EvaluateAndCompare(
return EvaluateWrapper(
executionContext,
"EvaluateDefaultInput",
() => _legacyManager.EvaluateDefaultInput(executionContext, inputName, token),
@@ -216,13 +217,27 @@ namespace GitHub.Runner.Worker
}
// Comparison helper methods
private TLegacy EvaluateAndCompare<TLegacy, TNew>(
private TLegacy EvaluateWrapper<TLegacy, TNew>(
IExecutionContext context,
string methodName,
Func<TLegacy> legacyEvaluator,
Func<TNew> newEvaluator,
Func<TLegacy, TNew, bool> resultComparer)
{
// Cutover: use only the new evaluator, convert result to legacy type
if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CutoverWorkflowParser) ?? false)
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_CUTOVER_WORKFLOW_PARSER")))
{
var newResult = newEvaluator();
if (typeof(TLegacy) == typeof(TNew))
{
return (TLegacy)(object)newResult;
}
var json = StringUtil.ConvertToJson(newResult, Newtonsoft.Json.Formatting.None);
return StringUtil.ConvertFromJson<TLegacy>(json);
}
// Legacy only?
if (!((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false)
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER"))))

View File

@@ -379,7 +379,14 @@ namespace GitHub.Runner.Worker
{
prefix = PipelineTemplateConstants.RunDisplayPrefix;
var repositoryReference = action.Reference as RepositoryPathReference;
var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
var pathString = string.Empty;
if (!string.IsNullOrEmpty(repositoryReference.Path))
{
// For local actions (Name is empty), don't prepend "/" to avoid "/./"
pathString = string.IsNullOrEmpty(repositoryReference.Name)
? repositoryReference.Path
: $"/{repositoryReference.Path}";
}
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
tokenToParse = new StringToken(null, null, null, repoString);

View File

@@ -499,7 +499,7 @@ namespace GitHub.Runner.Worker
PublishStepTelemetry();
if (_record.RecordType == "Task")
if (_record.RecordType == ExecutionContextType.Task)
{
var stepResult = new StepResult
{
@@ -532,6 +532,25 @@ namespace GitHub.Runner.Worker
Global.StepsResult.Add(stepResult);
}
if (Global.Variables.GetBoolean(Constants.Runner.Features.SendJobLevelAnnotations) ?? false)
{
if (_record.RecordType == ExecutionContextType.Job)
{
_record.Issues?.ForEach(issue =>
{
var annotation = issue.ToAnnotation();
if (annotation != null)
{
Global.JobAnnotations.Add(annotation.Value);
if (annotation.Value.IsInfrastructureIssue && string.IsNullOrEmpty(Global.InfrastructureFailureCategory))
{
Global.InfrastructureFailureCategory = issue.Category;
}
}
});
}
}
if (Root != this)
{
// only dispose TokenSource for step level ExecutionContext
@@ -1397,7 +1416,8 @@ namespace GitHub.Runner.Worker
public static IPipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null)
{
// 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"))
|| (context.Global.Variables.GetBoolean(Constants.Runner.Features.CutoverWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_CUTOVER_WORKFLOW_PARSER")))
{
return (context as ExecutionContext).ToPipelineTemplateEvaluatorInternal(traceWriter);
}

View File

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

View File

@@ -191,19 +191,13 @@ namespace GitHub.Runner.Worker.Handlers
ArgUtil.Directory(tempWorkflowDirectory, nameof(tempWorkflowDirectory));
container.MountVolumes.Add(new MountVolume("/var/run/docker.sock", "/var/run/docker.sock"));
if (FeatureManager.IsContainerActionRunnerTempEnabled(ExecutionContext.Global.Variables))
{
container.MountVolumes.Add(new MountVolume(tempDirectory, "/github/runner_temp"));
}
container.MountVolumes.Add(new MountVolume(tempDirectory, "/github/runner_temp"));
container.MountVolumes.Add(new MountVolume(tempHomeDirectory, "/github/home"));
container.MountVolumes.Add(new MountVolume(tempWorkflowDirectory, "/github/workflow"));
container.MountVolumes.Add(new MountVolume(tempFileCommandDirectory, "/github/file_commands"));
container.MountVolumes.Add(new MountVolume(defaultWorkingDirectory, "/github/workspace"));
if (FeatureManager.IsContainerActionRunnerTempEnabled(ExecutionContext.Global.Variables))
{
container.AddPathTranslateMapping(tempDirectory, "/github/runner_temp");
}
container.AddPathTranslateMapping(tempDirectory, "/github/runner_temp");
container.AddPathTranslateMapping(tempHomeDirectory, "/github/home");
container.AddPathTranslateMapping(tempWorkflowDirectory, "/github/workflow");
container.AddPathTranslateMapping(tempFileCommandDirectory, "/github/file_commands");
@@ -245,6 +239,14 @@ namespace GitHub.Runner.Worker.Handlers
Environment["ACTIONS_RESULTS_URL"] = resultsUrl;
}
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
{
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))
{
Environment["ACTIONS_ORCHESTRATION_ID"] = orchestrationId;
}
}
foreach (var variable in this.Environment)
{
container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value);

View File

@@ -77,6 +77,14 @@ namespace GitHub.Runner.Worker.Handlers
Environment["ACTIONS_CACHE_SERVICE_V2"] = bool.TrueString;
}
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
{
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))
{
Environment["ACTIONS_ORCHESTRATION_ID"] = orchestrationId;
}
}
// Resolve the target script.
string target = null;
if (stage == ActionRunStage.Main)

View File

@@ -318,6 +318,14 @@ namespace GitHub.Runner.Worker.Handlers
Environment["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = systemConnection.Authorization.Parameters[EndpointAuthorizationParameters.AccessToken];
}
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
{
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))
{
Environment["ACTIONS_ORCHESTRATION_ID"] = orchestrationId;
}
}
ExecutionContext.Debug($"{fileName} {arguments}");
Inputs.TryGetValue("standardInInput", out var standardInInput);

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Test")]

View File

@@ -112,6 +112,13 @@ namespace GitHub.Runner.Worker
groupName = "Machine Setup Info";
}
// not output internal groups
if (groupName.StartsWith("_internal_", StringComparison.OrdinalIgnoreCase))
{
jobContext.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.General, Message = info.Detail });
continue;
}
context.Output($"##[group]{groupName}");
var multiLines = info.Detail.Replace("\r\n", "\n").TrimEnd('\n').Split('\n');
foreach (var line in multiLines)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using GitHub.Actions.WorkflowParser;
using GitHub.DistributedTask.Expressions2;
@@ -19,6 +19,7 @@ namespace GitHub.Runner.Worker
private WorkflowTemplateEvaluator _newEvaluator;
private IExecutionContext _context;
private Tracing _trace;
private bool _cutover;
public PipelineTemplateEvaluatorWrapper(
IHostContext hostContext,
@@ -29,6 +30,8 @@ namespace GitHub.Runner.Worker
ArgUtil.NotNull(context, nameof(context));
_context = context;
_trace = hostContext.GetTrace(nameof(PipelineTemplateEvaluatorWrapper));
_cutover = (context.Global.Variables.GetBoolean(Constants.Runner.Features.CutoverWorkflowParser) ?? false)
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_CUTOVER_WORKFLOW_PARSER"));
if (traceWriter == null)
{
@@ -55,7 +58,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateStepContinueOnError",
() => _legacyEvaluator.EvaluateStepContinueOnError(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateStepContinueOnError(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -67,7 +70,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateStepDisplayName",
() => _legacyEvaluator.EvaluateStepDisplayName(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateStepName(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -80,7 +83,7 @@ namespace GitHub.Runner.Worker
IList<IFunctionInfo> expressionFunctions,
StringComparer keyComparer)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateStepEnvironment",
() => _legacyEvaluator.EvaluateStepEnvironment(token, contextData, expressionFunctions, keyComparer),
() => _newEvaluator.EvaluateStepEnvironment(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions), keyComparer),
@@ -93,7 +96,7 @@ namespace GitHub.Runner.Worker
IList<IFunctionInfo> expressionFunctions,
IEnumerable<KeyValuePair<string, object>> expressionState)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateStepIf",
() => _legacyEvaluator.EvaluateStepIf(token, contextData, expressionFunctions, expressionState),
() => _newEvaluator.EvaluateStepIf(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions), expressionState),
@@ -105,7 +108,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateStepInputs",
() => _legacyEvaluator.EvaluateStepInputs(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateStepInputs(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -117,7 +120,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateStepTimeout",
() => _legacyEvaluator.EvaluateStepTimeout(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateStepTimeout(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -129,7 +132,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateJobContainer",
() => _legacyEvaluator.EvaluateJobContainer(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateJobContainer(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -141,7 +144,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateJobOutput",
() => _legacyEvaluator.EvaluateJobOutput(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateJobOutputs(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -153,7 +156,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateEnvironmentUrl",
() => _legacyEvaluator.EvaluateEnvironmentUrl(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateJobEnvironmentUrl(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -165,7 +168,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateJobDefaultsRun",
() => _legacyEvaluator.EvaluateJobDefaultsRun(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateJobDefaultsRun(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -177,7 +180,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateJobServiceContainers",
() => _legacyEvaluator.EvaluateJobServiceContainers(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateJobServiceContainers(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -189,7 +192,7 @@ namespace GitHub.Runner.Worker
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions)
{
return EvaluateAndCompare(
return EvaluateWrapper(
"EvaluateJobSnapshotRequest",
() => _legacyEvaluator.EvaluateJobSnapshotRequest(token, contextData, expressionFunctions),
() => _newEvaluator.EvaluateSnapshot(string.Empty, ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
@@ -216,12 +219,25 @@ namespace GitHub.Runner.Worker
}
}
private TLegacy EvaluateAndCompare<TLegacy, TNew>(
private TLegacy EvaluateWrapper<TLegacy, TNew>(
string methodName,
Func<TLegacy> legacyEvaluator,
Func<TNew> newEvaluator,
Func<TLegacy, TNew, bool> resultComparer)
{
// Cutover: use only the new evaluator, convert result to legacy type
if (_cutover)
{
var newResult = newEvaluator();
if (typeof(TLegacy) == typeof(TNew))
{
return (TLegacy)(object)newResult;
}
var json = StringUtil.ConvertToJson(newResult, Newtonsoft.Json.Formatting.None);
return StringUtil.ConvertFromJson<TLegacy>(json);
}
// Legacy evaluator
var legacyException = default(Exception);
var legacyResult = default(TLegacy);

View File

@@ -9,6 +9,7 @@ namespace GitHub.DistributedTask.Expressions2
{
static ExpressionConstants()
{
AddFunction<Case>("case", 3, Byte.MaxValue);
AddFunction<Contains>("contains", 2, 2);
AddFunction<EndsWith>("endsWith", 2, 2);
AddFunction<Format>("format", 1, Byte.MaxValue);

View File

@@ -17,9 +17,10 @@ namespace GitHub.DistributedTask.Expressions2
String expression,
ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions)
IEnumerable<IFunctionInfo> functions,
Boolean allowCaseFunction = true)
{
var context = new ParseContext(expression, trace, namedValues, functions);
var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction);
context.Trace.Info($"Parsing expression: <{expression}>");
return CreateTree(context);
}
@@ -349,6 +350,10 @@ namespace GitHub.DistributedTask.Expressions2
{
throw new ParseException(ParseExceptionKind.TooManyParameters, token: @operator, expression: context.Expression);
}
else if (functionInfo.Name.Equals("case", StringComparison.OrdinalIgnoreCase) && function.Parameters.Count % 2 == 0)
{
throw new ParseException(ParseExceptionKind.EvenParameters, token: @operator, expression: context.Expression);
}
}
/// <summary>
@@ -411,6 +416,12 @@ namespace GitHub.DistributedTask.Expressions2
String name,
out IFunctionInfo functionInfo)
{
if (String.Equals(name, "case", StringComparison.OrdinalIgnoreCase) && !context.AllowCaseFunction)
{
functionInfo = null;
return false;
}
return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) ||
context.ExtensionFunctions.TryGetValue(name, out functionInfo);
}
@@ -418,6 +429,7 @@ namespace GitHub.DistributedTask.Expressions2
private sealed class ParseContext
{
public Boolean AllowUnknownKeywords;
public Boolean AllowCaseFunction;
public readonly String Expression;
public readonly Dictionary<String, IFunctionInfo> ExtensionFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase);
public readonly Dictionary<String, INamedValueInfo> ExtensionNamedValues = new Dictionary<String, INamedValueInfo>(StringComparer.OrdinalIgnoreCase);
@@ -433,7 +445,8 @@ namespace GitHub.DistributedTask.Expressions2
ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions,
Boolean allowUnknownKeywords = false)
Boolean allowUnknownKeywords = false,
Boolean allowCaseFunction = true)
{
Expression = expression ?? String.Empty;
if (Expression.Length > ExpressionConstants.MaxLength)
@@ -454,6 +467,7 @@ namespace GitHub.DistributedTask.Expressions2
LexicalAnalyzer = new LexicalAnalyzer(Expression);
AllowUnknownKeywords = allowUnknownKeywords;
AllowCaseFunction = allowCaseFunction;
}
private class NoOperationTraceWriter : ITraceWriter

View File

@@ -29,6 +29,9 @@ namespace GitHub.DistributedTask.Expressions2
case ParseExceptionKind.TooManyParameters:
description = "Too many parameters supplied";
break;
case ParseExceptionKind.EvenParameters:
description = "Even number of parameters supplied, requires an odd number of parameters";
break;
case ParseExceptionKind.UnexpectedEndOfExpression:
description = "Unexpected end of expression";
break;

View File

@@ -6,6 +6,7 @@
ExceededMaxLength,
TooFewParameters,
TooManyParameters,
EvenParameters,
UnexpectedEndOfExpression,
UnexpectedSymbol,
UnrecognizedFunction,

View File

@@ -0,0 +1,45 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.Expressions.Data;
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
{
internal sealed class Case : Function
{
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
// Validate argument count - must be odd (pairs of predicate-result plus default)
if (Parameters.Count % 2 == 0)
{
throw new InvalidOperationException("case requires an odd number of arguments");
}
// Evaluate predicate-result pairs
for (var i = 0; i < Parameters.Count - 1; i += 2)
{
var predicate = Parameters[i].Evaluate(context);
// Predicate must be a boolean
if (predicate.Kind != ValueKind.Boolean)
{
throw new InvalidOperationException("case predicate must evaluate to a boolean value");
}
// If predicate is true, return the corresponding result
if ((Boolean)predicate.Value)
{
var result = Parameters[i + 1].Evaluate(context);
return result.Value;
}
}
// No predicate matched, return default (last argument)
var defaultResult = Parameters[Parameters.Count - 1].Evaluate(context);
return defaultResult.Value;
}
}
}

View File

@@ -86,6 +86,12 @@ namespace GitHub.DistributedTask.ObjectTemplating
internal ITraceWriter TraceWriter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the case expression function is allowed.
/// Defaults to true. Set to false to disable the case function.
/// </summary>
internal Boolean AllowCaseFunction { get; set; } = true;
private IDictionary<String, Int32> FileIds
{
get

View File

@@ -57,7 +57,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
@@ -94,7 +94,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
@@ -123,7 +123,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
@@ -152,7 +152,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,

View File

@@ -663,7 +663,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
var node = default(ExpressionNode);
try
{
node = expressionParser.CreateTree(condition, null, namedValues, functions) as ExpressionNode;
node = expressionParser.CreateTree(condition, null, namedValues, functions, allowCaseFunction: context.AllowCaseFunction) as ExpressionNode;
}
catch (Exception ex)
{

View File

@@ -421,7 +421,7 @@
"mapping": {
"properties": {
"image": "string",
"options": "non-empty-string",
"options": "string",
"env": "container-env",
"ports": "sequence-of-non-empty-string",
"volumes": "sequence-of-non-empty-string",

View File

@@ -10,6 +10,7 @@ namespace GitHub.Actions.Expressions
{
static ExpressionConstants()
{
AddFunction<Case>("case", 3, Byte.MaxValue);
AddFunction<Contains>("contains", 2, 2);
AddFunction<EndsWith>("endsWith", 2, 2);
AddFunction<Format>("format", 1, Byte.MaxValue);

View File

@@ -17,9 +17,10 @@ namespace GitHub.Actions.Expressions
String expression,
ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions)
IEnumerable<IFunctionInfo> functions,
Boolean allowCaseFunction = true)
{
var context = new ParseContext(expression, trace, namedValues, functions);
var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction: allowCaseFunction);
context.Trace.Info($"Parsing expression: <{expression}>");
return CreateTree(context);
}
@@ -349,6 +350,10 @@ namespace GitHub.Actions.Expressions
{
throw new ParseException(ParseExceptionKind.TooManyParameters, token: @operator, expression: context.Expression);
}
else if (functionInfo.Name.Equals("case", StringComparison.OrdinalIgnoreCase) && function.Parameters.Count % 2 == 0)
{
throw new ParseException(ParseExceptionKind.EvenParameters, token: @operator, expression: context.Expression);
}
}
/// <summary>
@@ -411,6 +416,12 @@ namespace GitHub.Actions.Expressions
String name,
out IFunctionInfo functionInfo)
{
if (String.Equals(name, "case", StringComparison.OrdinalIgnoreCase) && !context.AllowCaseFunction)
{
functionInfo = null;
return false;
}
return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) ||
context.ExtensionFunctions.TryGetValue(name, out functionInfo);
}
@@ -418,6 +429,7 @@ namespace GitHub.Actions.Expressions
private sealed class ParseContext
{
public Boolean AllowUnknownKeywords;
public Boolean AllowCaseFunction;
public readonly String Expression;
public readonly Dictionary<String, IFunctionInfo> ExtensionFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase);
public readonly Dictionary<String, INamedValueInfo> ExtensionNamedValues = new Dictionary<String, INamedValueInfo>(StringComparer.OrdinalIgnoreCase);
@@ -433,7 +445,8 @@ namespace GitHub.Actions.Expressions
ITraceWriter trace,
IEnumerable<INamedValueInfo> namedValues,
IEnumerable<IFunctionInfo> functions,
Boolean allowUnknownKeywords = false)
Boolean allowUnknownKeywords = false,
Boolean allowCaseFunction = true)
{
Expression = expression ?? String.Empty;
if (Expression.Length > ExpressionConstants.MaxLength)
@@ -454,6 +467,7 @@ namespace GitHub.Actions.Expressions
LexicalAnalyzer = new LexicalAnalyzer(Expression);
AllowUnknownKeywords = allowUnknownKeywords;
AllowCaseFunction = allowCaseFunction;
}
private class NoOperationTraceWriter : ITraceWriter
@@ -468,4 +482,4 @@ namespace GitHub.Actions.Expressions
}
}
}
}
}

View File

@@ -29,6 +29,9 @@ namespace GitHub.Actions.Expressions
case ParseExceptionKind.TooManyParameters:
description = "Too many parameters supplied";
break;
case ParseExceptionKind.EvenParameters:
description = "Even number of parameters supplied, requires an odd number of parameters";
break;
case ParseExceptionKind.UnexpectedEndOfExpression:
description = "Unexpected end of expression";
break;

View File

@@ -6,6 +6,7 @@ namespace GitHub.Actions.Expressions
ExceededMaxLength,
TooFewParameters,
TooManyParameters,
EvenParameters,
UnexpectedEndOfExpression,
UnexpectedSymbol,
UnrecognizedFunction,

View File

@@ -0,0 +1,45 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.Expressions.Data;
namespace GitHub.Actions.Expressions.Sdk.Functions
{
internal sealed class Case : Function
{
protected sealed override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
// Validate argument count - must be odd (pairs of predicate-result plus default)
if (Parameters.Count % 2 == 0)
{
throw new InvalidOperationException("case requires an odd number of arguments");
}
// Evaluate predicate-result pairs
for (var i = 0; i < Parameters.Count - 1; i += 2)
{
var predicate = Parameters[i].Evaluate(context);
// Predicate must be a boolean
if (predicate.Kind != ValueKind.Boolean)
{
throw new InvalidOperationException("case predicate must evaluate to a boolean value");
}
// If predicate is true, return the corresponding result
if ((Boolean)predicate.Value)
{
var result = Parameters[i + 1].Evaluate(context);
return result.Value;
}
}
// No predicate matched, return default (last argument)
var defaultResult = Parameters[Parameters.Count - 1].Evaluate(context);
return defaultResult.Value;
}
}
}

View File

@@ -18,19 +18,19 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.26.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.27.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="8.0.0" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.2" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<PackageReference Include="Minimatch" Version="2.0.0" />
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
<PackageReference Include="System.Formats.Asn1" Version="10.0.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -1775,7 +1775,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion
var node = default(ExpressionNode);
try
{
node = expressionParser.CreateTree(condition, null, namedValues, functions) as ExpressionNode;
node = expressionParser.CreateTree(condition, null, namedValues, functions, allowCaseFunction: context.AllowCaseFunction) as ExpressionNode;
}
catch (Exception ex)
{

View File

@@ -113,6 +113,12 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating
/// </summary>
internal Boolean StrictJsonParsing { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the case expression function is allowed.
/// Defaults to true. Set to false to disable the case function.
/// </summary>
internal Boolean AllowCaseFunction { get; set; } = true;
internal ITraceWriter TraceWriter { get; set; }
private IDictionary<String, Int32> FileIds

View File

@@ -55,7 +55,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
@@ -93,7 +93,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
@@ -123,7 +123,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
@@ -153,7 +153,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,

View File

@@ -2593,7 +2593,7 @@
"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."
},
"options": {
"type": "non-empty-string",
"type": "string",
"description": "Use `jobs.<job_id>.container.options` to configure additional Docker container resource options."
},
"env": "container-env",

View File

@@ -0,0 +1,456 @@
using GitHub.Actions.WorkflowParser;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using LegacyContextData = GitHub.DistributedTask.Pipelines.ContextData;
using LegacyExpressions = GitHub.DistributedTask.Expressions2;
using Moq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
/// <summary>
/// Tests for parser comparison wrapper classes.
/// </summary>
public sealed class ActionManifestParserComparisonL0
{
private CancellationTokenSource _ecTokenSource;
private Mock<IExecutionContext> _ec;
private TestHostContext _hc;
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ConvertToLegacySteps_ProducesCorrectSteps_WithExplicitPropertyMapping()
{
try
{
// Arrange - Test that ActionManifestManagerWrapper properly converts new steps to legacy format
Setup();
// Enable comparison feature
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
// Register required services
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(), "conditional_composite_action.yml");
// Act - Load through the wrapper (which internally converts)
var result = wrapper.Load(_ec.Object, manifestPath);
// Assert
Assert.NotNull(result);
Assert.Equal(ActionExecutionType.Composite, result.Execution.ExecutionType);
var compositeExecution = result.Execution as CompositeActionExecutionData;
Assert.NotNull(compositeExecution);
Assert.NotNull(compositeExecution.Steps);
Assert.Equal(6, compositeExecution.Steps.Count);
// Verify steps are NOT null (this was the bug - JSON round-trip produced nulls)
foreach (var step in compositeExecution.Steps)
{
Assert.NotNull(step);
Assert.NotNull(step.Reference);
Assert.IsType<GitHub.DistributedTask.Pipelines.ScriptReference>(step.Reference);
}
// Verify step with condition
var successStep = compositeExecution.Steps[2];
Assert.Equal("success-conditional", successStep.ContextName);
Assert.Equal("success()", successStep.Condition);
// Verify step with complex condition
var lastStep = compositeExecution.Steps[5];
Assert.Contains("inputs.exit-code == 1", lastStep.Condition);
Assert.Contains("failure()", lastStep.Condition);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateJobContainer_EmptyImage_BothParsersReturnNull()
{
try
{
// Arrange - Test that both parsers return null for empty container image at runtime
Setup();
var fileTable = new List<string>();
// Create legacy evaluator
var legacyTraceWriter = new GitHub.DistributedTask.ObjectTemplating.EmptyTraceWriter();
var schema = PipelineTemplateSchemaFactory.GetSchema();
var legacyEvaluator = new PipelineTemplateEvaluator(legacyTraceWriter, schema, fileTable);
// Create new evaluator
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
var newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, fileTable, features: null);
// Create a token representing an empty container image (simulates expression evaluated to empty string)
var emptyImageToken = new StringToken(null, null, null, "");
var contextData = new DictionaryContextData();
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
// Act - Call both evaluators
var legacyResult = legacyEvaluator.EvaluateJobContainer(emptyImageToken, contextData, expressionFunctions);
// Convert token for new evaluator
var newToken = new GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.StringToken(null, null, null, "");
var newContextData = new GitHub.Actions.Expressions.Data.DictionaryExpressionData();
var newExpressionFunctions = new List<GitHub.Actions.Expressions.IFunctionInfo>();
var newResult = newEvaluator.EvaluateJobContainer(newToken, newContextData, newExpressionFunctions);
// Assert - Both should return null for empty image (no container)
Assert.Null(legacyResult);
Assert.Null(newResult);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void FromJsonEmptyString_BothParsersFail_WithDifferentMessages()
{
// This test verifies that both parsers fail with different error messages when parsing fromJSON('')
// The comparison layer should treat these as semantically equivalent (both are JSON parse errors)
try
{
Setup();
var fileTable = new List<string>();
// Create legacy evaluator
var legacyTraceWriter = new GitHub.DistributedTask.ObjectTemplating.EmptyTraceWriter();
var schema = PipelineTemplateSchemaFactory.GetSchema();
var legacyEvaluator = new PipelineTemplateEvaluator(legacyTraceWriter, schema, fileTable);
// Create new evaluator
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
var newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, fileTable, features: null);
// Create expression token for fromJSON('')
var legacyToken = new BasicExpressionToken(null, null, null, "fromJson('')");
var newToken = new GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.BasicExpressionToken(null, null, null, "fromJson('')");
var contextData = new DictionaryContextData();
var newContextData = new GitHub.Actions.Expressions.Data.DictionaryExpressionData();
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
var newExpressionFunctions = new List<GitHub.Actions.Expressions.IFunctionInfo>();
// Act - Both should throw
Exception legacyException = null;
Exception newException = null;
try
{
legacyEvaluator.EvaluateStepDisplayName(legacyToken, contextData, expressionFunctions);
}
catch (Exception ex)
{
legacyException = ex;
}
try
{
newEvaluator.EvaluateStepName(newToken, newContextData, newExpressionFunctions);
}
catch (Exception ex)
{
newException = ex;
}
// Assert - Both threw exceptions
Assert.NotNull(legacyException);
Assert.NotNull(newException);
// Verify the error messages are different (which is why we need semantic comparison)
Assert.NotEqual(legacyException.Message, newException.Message);
// Verify both are JSON parse errors (contain JSON-related error indicators)
var legacyFullMsg = GetFullExceptionMessage(legacyException);
var newFullMsg = GetFullExceptionMessage(newException);
// At least one should contain indicators of JSON parsing failure
var legacyIsJsonError = legacyFullMsg.Contains("JToken") ||
legacyFullMsg.Contains("JsonReader") ||
legacyFullMsg.Contains("fromJson");
var newIsJsonError = newFullMsg.Contains("JToken") ||
newFullMsg.Contains("JsonReader") ||
newFullMsg.Contains("fromJson");
Assert.True(legacyIsJsonError, $"Legacy exception should be JSON error: {legacyFullMsg}");
Assert.True(newIsJsonError, $"New exception should be JSON error: {newFullMsg}");
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateWrapper_SkipsMismatchRecording_WhenCancellationOccursDuringEvaluation()
{
try
{
// Arrange - Test that mismatches are not recorded when cancellation state changes during evaluation
Setup();
// Enable comparison feature
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
// Create the wrapper
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
// Create a simple token for evaluation
var token = new StringToken(null, null, null, "test-value");
var contextData = new DictionaryContextData();
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
// First evaluation without cancellation - should work normally
var result1 = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
Assert.Equal("test-value", result1);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
// Now simulate a scenario where cancellation occurs during evaluation
// Cancel the token before next evaluation
_ecTokenSource.Cancel();
// Evaluate again - even if there were a mismatch, it should be skipped due to cancellation
var result2 = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
Assert.Equal("test-value", result2);
// Verify no mismatch was recorded (cancellation race detection should have prevented it)
// Note: In this test, both parsers return the same result, so there's no actual mismatch.
// The cancellation race detection is a safeguard for when results differ due to timing.
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateWrapper_DoesNotRecordMismatch_WhenResultsMatch()
{
try
{
// Arrange - Test that no mismatch is recorded when both parsers return matching results
Setup();
// Enable comparison feature
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
// Create the wrapper
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
// Create a simple token for evaluation
var token = new StringToken(null, null, null, "test-value");
var contextData = new DictionaryContextData();
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
// Evaluation without cancellation - should work normally and not record mismatch for matching results
var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
Assert.Equal("test-value", result);
// Since both parsers return the same result, no mismatch should be recorded
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CutoverFlag_UsesNewEvaluator_ForPipelineTemplateEvaluator()
{
try
{
// Arrange - Test that cutover flag causes the wrapper to use only the new evaluator
Setup();
// Enable cutover feature (not comparison)
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CutoverWorkflowParser, "true");
// Create the wrapper
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
// Create a simple token for evaluation
var token = new StringToken(null, null, null, "test-value");
var contextData = new DictionaryContextData();
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
// Act - Evaluate in cutover mode
var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
// Assert - Should get the correct result from the new evaluator
Assert.Equal("test-value", result);
// No mismatch should be recorded (comparison is skipped entirely in cutover mode)
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CutoverFlag_UsesNewManager_ForActionManifestLoad()
{
try
{
// Arrange - Test that cutover flag causes the manifest wrapper to use only the new manager
Setup();
// Enable cutover feature (not comparison)
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CutoverWorkflowParser, "true");
// Register required services
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(), "conditional_composite_action.yml");
// Act - Load through the wrapper in cutover mode
var result = wrapper.Load(_ec.Object, manifestPath);
// Assert - Should get the correct result from the new manager
Assert.NotNull(result);
Assert.Equal(ActionExecutionType.Composite, result.Execution.ExecutionType);
// No mismatch should be recorded (comparison is skipped in cutover mode)
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CutoverFlag_TakesPrecedence_OverCompareFlag()
{
try
{
// Arrange - Test that cutover flag takes precedence over compare flag
Setup();
// Enable both flags
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CutoverWorkflowParser, "true");
// Create the wrapper
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object);
var token = new StringToken(null, null, null, "test-value");
var contextData = new DictionaryContextData();
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
// Act - Evaluate (cutover should take precedence, skipping comparison entirely)
var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
// Assert - Should get correct result, no comparison mismatch recorded
Assert.Equal("test-value", result);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
private string GetFullExceptionMessage(Exception ex)
{
var messages = new List<string>();
var current = ex;
while (current != null)
{
messages.Add(current.Message);
current = current.InnerException;
}
return string.Join(" -> ", messages);
}
private void Setup([CallerMemberName] string name = "")
{
_ecTokenSource?.Dispose();
_ecTokenSource = new CancellationTokenSource();
_hc = new TestHostContext(this, name);
var expressionValues = new LegacyContextData.DictionaryContextData();
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
_ec = new Mock<IExecutionContext>();
_ec.Setup(x => x.Global)
.Returns(new GlobalContext
{
FileTable = new List<String>(),
Variables = new Variables(_hc, new Dictionary<string, VariableValue>()),
WriteDebug = true,
});
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
_ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
_ec.Setup(x => x.ExpressionFunctions).Returns(expressionFunctions);
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); });
}
private void Teardown()
{
_hc?.Dispose();
}
}
}

View File

@@ -316,6 +316,94 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("${{ matrix.node }}", _actionRunner.DisplayName);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateDisplayNameForLocalAction()
{
// Arrange
Setup();
var actionId = Guid.NewGuid();
var action = new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfAlias,
Path = "./"
}
};
_actionRunner.Action = action;
// Act
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
// Assert
Assert.True(validDisplayName);
Assert.True(updated);
Assert.Equal("Run ./", _actionRunner.DisplayName); // NOT "Run /./"
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateDisplayNameForLocalActionWithPath()
{
// Arrange
Setup();
var actionId = Guid.NewGuid();
var action = new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfAlias,
Path = "./.github/actions/my-action"
}
};
_actionRunner.Action = action;
// Act
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
// Assert
Assert.True(validDisplayName);
Assert.True(updated);
Assert.Equal("Run ./.github/actions/my-action", _actionRunner.DisplayName);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateDisplayNameForRemoteActionWithPath()
{
// Arrange
Setup();
var actionId = Guid.NewGuid();
var action = new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
Name = "owner/repo",
Path = "subdir",
Ref = "v1"
}
};
_actionRunner.Action = action;
// Act
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
// Assert
Assert.True(validDisplayName);
Assert.True(updated);
Assert.Equal("Run owner/repo/subdir@v1", _actionRunner.DisplayName);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -459,7 +547,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_handlerFactory = new Mock<IHandlerFactory>();
_defaultStepHost = new Mock<IDefaultStepHost>();
var actionManifestLegacy = new ActionManifestManagerLegacy();
actionManifestLegacy.Initialize(_hc);
_hc.SetSingleton<IActionManifestManagerLegacy>(actionManifestLegacy);

View File

@@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout"
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
PACKAGE_DIR="$SCRIPT_DIR/../_package"
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
DOTNETSDK_VERSION="8.0.416"
DOTNETSDK_VERSION="8.0.417"
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
RUNNER_VERSION=$(cat runnerversion)

View File

@@ -1,5 +1,5 @@
{
"sdk": {
"version": "8.0.416"
"version": "8.0.417"
}
}

View File

@@ -1 +1 @@
2.330.0
2.331.0