Compare commits

..

41 Commits

Author SHA1 Message Date
Francesco Renzi
c9418a620c PR Feedback 2026-03-19 16:14:08 +00:00
Francesco Renzi
4dc0a8d1fd PR Feedback 2026-03-18 17:00:44 +00:00
Francesco Renzi
ba771350ac merge debugger and server 2026-03-18 15:20:56 +00:00
Francesco Renzi
a86ed1ec8f PR Feedback 2026-03-18 11:06:27 +00:00
Francesco Renzi
d15ab3d8fa Apply suggestions from code review
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2026-03-17 15:30:19 +00:00
Francesco Renzi
fa298ebe45 Add connection telemetry 2026-03-17 14:13:18 +00:00
Francesco Renzi
da35402572 Merge DapDebugger and DapDebugSession 2026-03-17 10:49:14 +00:00
Francesco Renzi
e3c0a6e119 PR Feedback 2026-03-17 09:49:09 +00:00
Francesco Renzi
f9919b2ba4 PR Feedback 2026-03-16 13:51:18 +00:00
Francesco Renzi
437e20db7d Fail the job if no connection in 15 minutes 2026-03-16 11:49:07 +00:00
Francesco Renzi
b36d9a6c92 PR feedback 2026-03-16 11:17:24 +00:00
Francesco Renzi
4bf2b29367 Add DapDebugger facade 2026-03-16 10:48:54 +00:00
Francesco Renzi
7f39d40e38 Update src/Runner.Worker/JobRunner.cs
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2026-03-16 09:18:06 +00:00
Francesco Renzi
9cd74b0f26 ci 2026-03-13 14:19:47 +00:00
Francesco Renzi
9d33c82d61 volatile state 2026-03-13 11:11:09 +00:00
Francesco Renzi
7d0f26a557 encoding casting 2026-03-13 11:08:43 +00:00
Francesco Renzi
e11d6cfa59 lock state 2026-03-13 09:30:09 +00:00
Francesco Renzi
00bde90018 remove waits 2026-03-13 09:10:35 +00:00
Francesco Renzi
5bad8cb359 Mask step display names 2026-03-13 09:10:14 +00:00
Francesco Renzi
8d1e06f436 Remove centralized masking 2026-03-13 08:47:38 +00:00
Francesco Renzi
649dc74be3 More tests 2026-03-13 08:34:24 +00:00
Francesco Renzi
75760d1f34 Centralize outbound DAP masking and harden secrets scope
- Add centralized secret masking in DapServer.SendMessageInternal so all
  outbound DAP payloads (responses, events) are masked before serialization,
  creating a single egress funnel that catches secrets regardless of caller.
- Redact the entire secrets scope in DapVariableProvider regardless of
  PipelineContextData type (NumberContextData, BooleanContextData, containers)
  not just StringContextData, closing the defense-in-depth gap.
- Null values under secrets scope are now also redacted.
- Existing per-call-site masking retained as defense-in-depth.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 17:20:38 +00:00
Francesco Renzi
e4406e035e Fix debug session race conditions and step-flow bugs
- Guard WaitForCommandAsync against resurrecting terminated sessions (H1)
- Mask exception messages in top-level DAP error responses (M1)
- Move isFirstStep=false outside try block to prevent continue breakage (M5)
- Guard OnJobCompleted with lock-internal state check to prevent duplicate events (M6)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 17:16:47 +00:00
Francesco Renzi
a8f3b9195d Harden DAP server
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 17:15:34 +00:00
Francesco Renzi
8d6b38a428 Add completions support and friendly errors for unsupported commands
Completions (SupportsCompletionsRequest = true):
- Respond to DAP 'completions' requests with our DSL commands
  (help, help("run"), run(...)) so they appear in the debug
  console autocomplete across all DAP clients
- Add CompletionsArguments, CompletionItem, and
  CompletionsResponseBody to DapMessages

Friendly error messages for unsupported stepping commands:
- stepIn: explain that Actions debug at the step level
- stepOut: suggest using 'continue'
- stepBack/reverseContinue: note 'not yet supported'
- pause: explain automatic pausing at step boundaries

The DAP spec does not provide a capability to hide stepIn/stepOut
buttons (they are considered fundamental operations). The best
server-side UX is clear error messages when clients send them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 11:38:20 +00:00
Francesco Renzi
860a919081 Fix expression expansion in REPL run command
The run() command was passing ${{ }} expressions literally to the
shell instead of evaluating them first. This caused scripts like
`run("echo ${{ github.job }}")` to fail with 'bad substitution'.

Fix: add ExpandExpressions() that finds each ${{ expr }} occurrence,
evaluates it individually via PipelineTemplateEvaluator, masks the
result through SecretMasker, and substitutes it into the script body
before writing the temp file — matching how ActionRunner evaluates
step inputs before ScriptHandler sees them.

Also expands expressions in DSL-provided env values so that
`env: { TOKEN: "${{ secrets.MY_TOKEN }}" }` works correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 11:16:12 +00:00
Francesco Renzi
b76917a8a0 Add L0 tests for REPL parser and session routing
Parser tests (DapReplParserL0, 22 tests):
- help: bare, case-insensitive, with topic
- run: simple script, with shell, env, working_directory, all options
- Edge cases: escaped quotes, commas in env values
- Errors: empty args, unquoted arg, unknown option, missing paren
- Non-DSL input falls through: expressions, wrapped expressions, empty
- Help text contains expected commands and options
- Internal helpers: SplitArguments with nested braces, empty env block

Session integration tests (DapDebugSessionL0, 4 tests):
- REPL help returns help text
- REPL non-DSL input falls through to expression evaluation
- REPL parse error returns error result (not a DAP error response)
- watch context still evaluates expressions (not routed through REPL)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 11:06:26 +00:00
Francesco Renzi
165fb90296 Wire REPL routing into DapDebugSession
Route `evaluate` requests by context:
- `repl` context → DSL parser → command dispatch (help/run)
- All other contexts (watch, hover, etc.) → expression evaluation

If REPL input doesn't match any DSL command, it falls through to
expression evaluation so the Debug Console also works for ad-hoc
`github.repository`-style queries.

Changes:
- HandleEvaluateAsync replaces the sync HandleEvaluate
- HandleReplInputAsync parses input through DapReplParser.TryParse
- DispatchReplCommandAsync dispatches HelpCommand and RunCommand
- DapReplExecutor is created alongside the DAP server reference
- Remove vestigial `await Task.CompletedTask` from HandleMessageAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 11:03:10 +00:00
Francesco Renzi
735dd69833 Add DapReplExecutor for run command execution
Implement the run command executor that makes REPL `run(...)` behave
like a real workflow `run:` step by reusing the runner's existing
infrastructure.

Key design choices:
- Shell resolution mirrors ScriptHandler: job defaults → explicit
  shell from DSL → platform default (bash→sh on Unix, pwsh→powershell
  on Windows)
- Script fixup via ScriptHandlerHelpers.FixUpScriptContents() adds
  the same error-handling preamble as a real step
- Environment is built from ExecutionContext.ExpressionValues[`env`]
  plus runtime context variables (GITHUB_*, RUNNER_*, etc.), with
  DSL-provided env overrides applied last
- Working directory defaults to $GITHUB_WORKSPACE
- Output is streamed in real time via DAP output events with secrets
  masked before emission through HostContext.SecretMasker
- Only the exit code is returned in the evaluate response (avoiding
  the prototype's double-output bug)
- Temp script files are cleaned up after execution

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 11:01:15 +00:00
Francesco Renzi
852e8721d0 Add DAP REPL command model and parser
Introduce a typed command model and hand-rolled parser for the debug
console DSL. The parser turns REPL input into HelpCommand or
RunCommand objects, keeping parsing separate from execution.

Ruby-like DSL syntax:
  help                              → general help
  help("run")                      → command-specific help
  run("echo hello")                → run with default shell
  run("echo $X", shell: "bash", env: { X: "1" })
                                    → run with explicit shell and env

Parser features:
- Handles escaped quotes, nested braces, and mixed arguments
- Keyword arguments: shell, env, working_directory
- Env blocks parsed as { KEY: "value", KEY2: "value2" }
- Returns null for non-DSL input (falls through to expression eval)
- Descriptive error messages for malformed input
- Help text scaffolding for discoverability

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 10:58:35 +00:00
Francesco Renzi
2a98a8c955 Add L0 tests for DAP expression evaluation
Provider tests (DapVariableProviderL0):
- Simple expression evaluation (github.repository)
- ${{ }} wrapper stripping
- Secret masking in evaluation results
- Graceful error for invalid expressions
- No-context returns descriptive message
- Empty expression returns empty string
- InferResultType classifies null/bool/number/object/string

Session integration tests (DapDebugSessionL0):
- evaluate request returns result when paused with context
- evaluate request returns graceful error when no step active
- evaluate request handles ${{ }} wrapper syntax

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 09:56:28 +00:00
Francesco Renzi
f31e1c7c43 Wire evaluate request into DapDebugSession
Add HandleEvaluate() that delegates expression evaluation to the
DapVariableProvider, keeping all masking centralized.

Changes:
- Register 'evaluate' in the command dispatch switch
- HandleEvaluate resolves frame context and delegates to
  DapVariableProvider.EvaluateExpression()
- Set SupportsEvaluateForHovers = true in capabilities so DAP
  clients enable hover tooltips and the Watch pane

No separate feature flag — the debugger is already gated by
EnableDebugger on the job context.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 09:51:45 +00:00
Francesco Renzi
1573e36a44 Add expression evaluation to DapVariableProvider
Add EvaluateExpression() that evaluates GitHub Actions expressions
using the runner's existing PipelineTemplateEvaluator infrastructure.

How it works:
- Strips ${{ }} wrapper if present
- Creates a BasicExpressionToken and evaluates via
  EvaluateStepDisplayName (supports the full expression language:
  functions, operators, context access)
- Masks the result through MaskSecrets() — same masking path used
  by scope inspection
- Returns a structured EvaluateResponseBody with type inference
- Catches evaluation errors and returns masked error messages

Also adds InferResultType() helper for DAP type hints.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 09:50:39 +00:00
Francesco Renzi
0d33fd1930 Add L0 tests for DAP scope inspection and secret masking
Provider tests (DapVariableProviderL0):
- Scope discovery: empty context, populated scopes, variable count,
  stable reference IDs, secrets presentation hint
- Variable types: string, boolean, number, null handling
- Nested expansion: dictionaries and arrays with child drilling
- Secret masking: redacted values in secrets scope, SecretMasker
  integration for non-secret scopes, MaskSecrets delegation
- Reset: stale nested references invalidated after Reset()
- EvaluateName: dot-path expression syntax

Session integration tests (DapDebugSessionL0):
- Scopes request returns scopes from step execution context
- Variables request returns variables from step execution context
- Scopes request returns empty when no step is active
- Secrets values are redacted through the full request path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 07:13:15 +00:00
Francesco Renzi
2c65db137a Wire DapVariableProvider into DapDebugSession for scope inspection
Replace the stub HandleScopes/HandleVariables implementations that
returned empty lists with real delegation to DapVariableProvider.

Changes:
- DapDebugSession now creates a DapVariableProvider on Initialize()
- HandleScopes() resolves the execution context for the requested
  frame and delegates to the provider
- HandleVariables() delegates to the provider for both top-level
  scope references and nested dynamic references
- GetExecutionContextForFrame() maps frame IDs to contexts:
  frame 1 = current step, frames 1000+ = completed (no live context)
- Provider is reset on each new step to invalidate stale nested refs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 07:01:49 +00:00
Francesco Renzi
3d8c844883 Add DapVariableProvider for scope inspection with centralized masking
Introduce a reusable component that maps runner ExpressionValues and
PipelineContextData into DAP scopes and variables. This is the single
point where execution-context values are materialized for the debugger.

Key design decisions:
- Fixed scope reference IDs (1–100) for the 10 well-known scopes
  (github, env, runner, job, steps, secrets, inputs, vars, matrix, needs)
- Dynamic reference IDs (101+) for lazy nested object/array expansion
- All string values pass through HostContext.SecretMasker.MaskSecrets()
- The secrets scope is intentionally opaque: keys shown, values replaced
  with a constant redaction marker
- MaskSecrets() is public so future DAP features (evaluate, REPL) can
  reuse it without duplicating masking policy

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-12 07:00:01 +00:00
Francesco Renzi
915e13c842 Integrate DAP debugger into JobRunner and StepsRunner 2026-03-11 08:56:08 -07:00
Francesco Renzi
17b05ddaa4 Add minimal DAP debug session with next/continue support 2026-03-11 08:55:54 -07:00
Francesco Renzi
9737dfadd5 Add DAP TCP server with reconnection support 2026-03-11 08:55:41 -07:00
Francesco Renzi
cca15de3b3 Add DAP protocol message types and service interfaces 2026-03-11 08:55:17 -07:00
Francesco Renzi
8b1b23b5ce Get EnableDebugger from job context 2026-03-10 04:13:39 -07:00
57 changed files with 6461 additions and 1632 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,35 @@
## What's Changed
* Log inner exception message. by @TingluoHuang in https://github.com/actions/runner/pull/4265
* Fix composite post-step marker display names by @ericsciple in https://github.com/actions/runner/pull/4267
* Bump actions/download-artifact from 7 to 8 by @dependabot[bot] in https://github.com/actions/runner/pull/4269
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4272
* Avoid throw in SelfUpdaters. by @TingluoHuang in https://github.com/actions/runner/pull/4274
* Fix parser comparison mismatches by @ericsciple in https://github.com/actions/runner/pull/4273
* Devcontainer: bump base image Ubuntu version by @MaxHorstmann in https://github.com/actions/runner/pull/4277
* Support `entrypoint` and `command` for service containers by @ericsciple in https://github.com/actions/runner/pull/4276
* Bump actions/upload-artifact from 6 to 7 by @dependabot[bot] in https://github.com/actions/runner/pull/4270
* Bump docker/login-action from 3 to 4 by @dependabot[bot] in https://github.com/actions/runner/pull/4278
* Fix positional arg bug in ExpressionParser.CreateTree by @ericsciple in https://github.com/actions/runner/pull/4279
* Bump docker/build-push-action from 6 to 7 by @dependabot[bot] in https://github.com/actions/runner/pull/4283
* Bump docker/setup-buildx-action from 3 to 4 by @dependabot[bot] in https://github.com/actions/runner/pull/4282
* Bump actions/attest-build-provenance from 3 to 4 by @dependabot[bot] in https://github.com/actions/runner/pull/4266
* Bump @stylistic/eslint-plugin from 5.9.0 to 5.10.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4281
* Update Docker to v29.3.0 and Buildx to v0.32.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4286
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4287
* Fix cancellation token race during parser comparison by @ericsciple in https://github.com/actions/runner/pull/4280
* Bump @typescript-eslint/eslint-plugin from 8.47.0 to 8.54.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4230
* Exit with specified exit code when runner is outdated by @nikola-jokic in https://github.com/actions/runner/pull/4285
* Report infra_error for action download failures. by @TingluoHuang in https://github.com/actions/runner/pull/4294
* Update dotnet sdk to latest version @8.0.419 by @github-actions[bot] in https://github.com/actions/runner/pull/4301
* Node 24 enforcement + Linux ARM32 deprecation support by @salmanmkc in https://github.com/actions/runner/pull/4303
* Bump @typescript-eslint/eslint-plugin from 8.54.0 to 8.57.1 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4304
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4200
* Update dotnet sdk to latest version @8.0.417 by @github-actions[bot] in https://github.com/actions/runner/pull/4201
* Bump System.Formats.Asn1 and System.Security.Cryptography.Pkcs by @dependabot[bot] in https://github.com/actions/runner/pull/4202
* Allow empty container options by @ericsciple in https://github.com/actions/runner/pull/4208
* Update Docker to v29.1.5 and Buildx to v0.31.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4212
* Report job level annotations by @TingluoHuang in https://github.com/actions/runner/pull/4216
* Fix local action display name showing `Run /./` instead of `Run ./` by @ericsciple in https://github.com/actions/runner/pull/4218
* Update Docker to v29.2.0 and Buildx to v0.31.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4219
* Add support for libssl3 and libssl3t64 for newer Debian/Ubuntu versions by @nekketsuuu in https://github.com/actions/runner/pull/4213
* Validate work dir during runner start up. by @TingluoHuang in https://github.com/actions/runner/pull/4227
* Bump hook to 0.8.1 by @nikola-jokic in https://github.com/actions/runner/pull/4222
* Support return job result as exitcode in hosted runner. by @TingluoHuang in https://github.com/actions/runner/pull/4233
* Add telemetry tracking for deprecated set-output and save-state commands by @ericsciple in https://github.com/actions/runner/pull/4221
* Fix parser comparison mismatches by @ericsciple in https://github.com/actions/runner/pull/4220
* Remove unnecessary connection test during some registration flows by @zarenner in https://github.com/actions/runner/pull/4244
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4249
* Update dotnet sdk to latest version @8.0.418 by @github-actions[bot] in https://github.com/actions/runner/pull/4250
* 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
* @MaxHorstmann made their first contribution in https://github.com/actions/runner/pull/4277
* @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.332.0...v2.333.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.
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.

View File

@@ -1 +1 @@
2.333.0
<Update to ./src/runnerversion when creating release>

View File

@@ -12,9 +12,9 @@
"@actions/glob": "^0.4.0"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^5.10.0",
"@stylistic/eslint-plugin": "^5.9.0",
"@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vercel/ncc": "^0.38.3",
"eslint": "^8.47.0",
@@ -93,9 +93,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
"integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
"dev": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@@ -228,9 +228,9 @@
}
},
"node_modules/@stylistic/eslint-plugin": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
"integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.9.0.tgz",
"integrity": "sha512-FqqSkvDMYJReydrMhlugc71M76yLLQWNfmGq+SIlLa7N3kHp8Qq8i2PyWrVNAfjOyOIY+xv9XaaYwvVW7vroMA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
@@ -247,6 +247,19 @@
"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": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
@@ -308,19 +321,21 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
"integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
"integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/type-utils": "8.57.1",
"@typescript-eslint/utils": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"ignore": "^7.0.5",
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/type-utils": "8.47.0",
"@typescript-eslint/utils": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0"
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -330,8 +345,8 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.57.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"@typescript-eslint/parser": "^8.47.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
@@ -346,10 +361,11 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
@@ -358,16 +374,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz",
"integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz",
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"debug": "^4.4.3"
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/typescript-estree": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -377,19 +394,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz",
"integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz",
"integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.57.1",
"@typescript-eslint/types": "^8.57.1",
"debug": "^4.4.3"
"@typescript-eslint/tsconfig-utils": "^8.47.0",
"@typescript-eslint/types": "^8.47.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -403,13 +421,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz",
"integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz",
"integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1"
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -420,10 +439,11 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz",
"integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz",
"integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -436,16 +456,17 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz",
"integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz",
"integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1",
"@typescript-eslint/utils": "8.57.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/typescript-estree": "8.47.0",
"@typescript-eslint/utils": "8.47.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -455,15 +476,16 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
@@ -472,10 +494,11 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz",
"integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz",
"integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -485,20 +508,22 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz",
"integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz",
"integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.57.1",
"@typescript-eslint/tsconfig-utils": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.4.0"
"@typescript-eslint/project-service": "8.47.0",
"@typescript-eslint/tsconfig-utils": "8.47.0",
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -521,9 +546,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"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,
"dependencies": {
"balanced-match": "^4.0.2"
@@ -533,25 +558,26 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz",
"integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==",
"dev": true,
"dependencies": {
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
@@ -560,15 +586,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz",
"integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz",
"integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1"
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/typescript-estree": "8.47.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -578,18 +605,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz",
"integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz",
"integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.57.1",
"eslint-visitor-keys": "^5.0.0"
"@typescript-eslint/types": "8.47.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -600,12 +628,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -1121,10 +1150,11 @@
"dev": true
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
@@ -3989,9 +4019,9 @@
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
@@ -4289,51 +4319,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/titleize": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
@@ -4771,9 +4756,9 @@
}
},
"@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
"integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
"dev": true
},
"@eslint/eslintrc": {
@@ -4869,9 +4854,9 @@
}
},
"@stylistic/eslint-plugin": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
"integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.9.0.tgz",
"integrity": "sha512-FqqSkvDMYJReydrMhlugc71M76yLLQWNfmGq+SIlLa7N3kHp8Qq8i2PyWrVNAfjOyOIY+xv9XaaYwvVW7vroMA==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.9.1",
@@ -4882,6 +4867,12 @@
"picomatch": "^4.0.3"
},
"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": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
@@ -4923,19 +4914,20 @@
}
},
"@typescript-eslint/eslint-plugin": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
"integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
"integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/type-utils": "8.57.1",
"@typescript-eslint/utils": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"ignore": "^7.0.5",
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/type-utils": "8.47.0",
"@typescript-eslint/utils": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0"
"ts-api-utils": "^2.1.0"
},
"dependencies": {
"ignore": {
@@ -4945,98 +4937,99 @@
"dev": true
},
"ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"requires": {}
}
}
},
"@typescript-eslint/parser": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz",
"integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz",
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"debug": "^4.4.3"
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/typescript-estree": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/project-service": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz",
"integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz",
"integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==",
"dev": true,
"requires": {
"@typescript-eslint/tsconfig-utils": "^8.57.1",
"@typescript-eslint/types": "^8.57.1",
"debug": "^4.4.3"
"@typescript-eslint/tsconfig-utils": "^8.47.0",
"@typescript-eslint/types": "^8.47.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz",
"integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz",
"integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1"
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0"
}
},
"@typescript-eslint/tsconfig-utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz",
"integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz",
"integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==",
"dev": true,
"requires": {}
},
"@typescript-eslint/type-utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz",
"integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz",
"integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1",
"@typescript-eslint/utils": "8.57.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/typescript-estree": "8.47.0",
"@typescript-eslint/utils": "8.47.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"dependencies": {
"ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"requires": {}
}
}
},
"@typescript-eslint/types": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz",
"integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz",
"integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz",
"integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz",
"integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==",
"dev": true,
"requires": {
"@typescript-eslint/project-service": "8.57.1",
"@typescript-eslint/tsconfig-utils": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/visitor-keys": "8.57.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.4.0"
"@typescript-eslint/project-service": "8.47.0",
"@typescript-eslint/tsconfig-utils": "8.47.0",
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"dependencies": {
"balanced-match": {
@@ -5046,58 +5039,58 @@
"dev": true
},
"brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"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,
"requires": {
"balanced-match": "^4.0.2"
}
},
"minimatch": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz",
"integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==",
"dev": true,
"requires": {
"brace-expansion": "^5.0.2"
}
},
"ts-api-utils": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"requires": {}
}
}
},
"@typescript-eslint/utils": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz",
"integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz",
"integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.57.1",
"@typescript-eslint/types": "8.57.1",
"@typescript-eslint/typescript-estree": "8.57.1"
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/typescript-estree": "8.47.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz",
"integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==",
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz",
"integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.57.1",
"eslint-visitor-keys": "^5.0.0"
"@typescript-eslint/types": "8.47.0",
"eslint-visitor-keys": "^4.2.1"
},
"dependencies": {
"eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true
}
}
@@ -5445,9 +5438,9 @@
"dev": true
},
"debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"requires": {
"ms": "^2.1.3"
@@ -7396,9 +7389,9 @@
}
},
"semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true
},
"shebang-command": {
@@ -7594,31 +7587,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"requires": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"dependencies": {
"fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"requires": {}
},
"picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true
}
}
},
"titleize": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",

View File

@@ -35,9 +35,9 @@
"@actions/glob": "^0.4.0"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^5.10.0",
"@stylistic/eslint-plugin": "^5.9.0",
"@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vercel/ncc": "^0.38.3",
"eslint": "^8.47.0",

View File

@@ -6,7 +6,7 @@ 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.20.1"
NODE20_VERSION="20.20.0"
NODE24_VERSION="24.14.0"
get_abs_path() {

View File

@@ -10,13 +10,6 @@ if %ERRORLEVEL% EQU 0 (
exit /b 0
)
if "%ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE%"=="1" (
if %ERRORLEVEL% EQU 7 (
echo "Runner listener exit with deprecated version error code: %ERRORLEVEL%."
exit /b %ERRORLEVEL%
)
)
if %ERRORLEVEL% EQU 1 (
echo "Runner listener exit with terminated error, stop the service, no retry needed."
exit /b 0

View File

@@ -34,13 +34,11 @@ fi
updateFile="update.finished"
"$DIR"/bin/Runner.Listener run $*
returnCode=$?
if [[ $returnCode == 0 ]]; then
echo "Runner listener exit with 0 return code, stop the service, no retry needed."
exit 0
elif [[ "$ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE" == "1" && $returnCode -eq 7 ]]; then
echo "Runner listener exit with deprecated version exit code: ${returnCode}."
exit "$returnCode"
elif [[ $returnCode == 1 ]]; then
echo "Runner listener exit with terminated error, stop the service, no retry needed."
exit 0

View File

@@ -25,14 +25,7 @@ call "%~dp0run-helper.cmd" %*
if %ERRORLEVEL% EQU 1 (
echo "Restarting runner..."
goto :launch_helper
) else (
echo "Exiting runner..."
exit /b 0
)
if "%ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE%"=="1" (
if %ERRORLEVEL% EQU 7 (
echo "Exiting runner with deprecated version error code: %ERRORLEVEL%"
exit /b %ERRORLEVEL%
)
)
echo "Exiting runner..."
exit /b 0

View File

@@ -19,9 +19,6 @@ run() {
returnCode=$?
if [[ $returnCode -eq 2 ]]; then
echo "Restarting runner..."
elif [[ "$ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE" == "1" && $returnCode -eq 7 ]]; then
echo "Exiting runner..."
exit "$returnCode"
else
echo "Exiting runner..."
exit 0
@@ -45,9 +42,6 @@ runWithManualTrap() {
returnCode=$?
if [[ $returnCode -eq 2 ]]; then
echo "Restarting runner..."
elif [[ "$ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE" == "1" && $returnCode -eq 7 ]]; then
echo "Exiting runner..."
exit "$returnCode"
else
echo "Exiting runner..."
# Unregister signal handling before exit

View File

@@ -159,7 +159,6 @@ namespace GitHub.Runner.Common
// and the runner should be restarted. This is a temporary code and will be removed in the future after
// the runner is migrated to runner admin.
public const int RunnerConfigurationRefreshed = 6;
public const int RunnerVersionDeprecated = 7;
}
public static class Features
@@ -195,22 +194,8 @@ namespace GitHub.Runner.Common
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";
public static readonly string WarnOnNode20Flag = "actions.runner.warnonnode20";
// Feature flags for Linux ARM32 deprecation
public static readonly string DeprecateLinuxArm32Flag = "actions_runner_deprecate_linux_arm32";
public static readonly string KillLinuxArm32Flag = "actions_runner_kill_linux_arm32";
// Blog post URL for Node 20 deprecation
public static readonly string Node20DeprecationUrl = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/";
// Node 20 migration dates (hardcoded fallbacks, can be overridden via job variables)
public static readonly string Node24DefaultDate = "June 2nd, 2026";
public static readonly string Node20RemovalDate = "September 16th, 2026";
// Variable keys for server-overridable dates
public static readonly string Node24DefaultDateVariable = "actions_runner_node24_default_date";
public static readonly string Node20RemovalDateVariable = "actions_runner_node20_removal_date";
public static readonly string LinuxArm32DeprecationMessage = "Linux ARM32 runners are deprecated and will no longer be supported after {0}. Please migrate to a supported platform.";
}
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
@@ -292,7 +277,6 @@ namespace GitHub.Runner.Common
public static readonly string AllowUnsupportedCommands = "ACTIONS_ALLOW_UNSECURE_COMMANDS";
public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS";
public static readonly string RequireJobContainer = "ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER";
public static readonly string ReturnVersionDeprecatedExitCode = "ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE";
public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG";
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
}

View File

@@ -58,7 +58,7 @@ namespace GitHub.Runner.Common.Util
{
return (Constants.Runner.NodeMigration.Node24, null);
}
// Get environment variable details with source information
var forceNode24Details = GetEnvironmentVariableDetails(
Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment);
@@ -108,50 +108,14 @@ namespace GitHub.Runner.Common.Util
/// <summary>
/// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed.
/// Also handles ARM32 deprecation and kill switch phases.
/// </summary>
/// <param name="preferredVersion">The preferred Node version</param>
/// <param name="deprecateArm32">Feature flag indicating ARM32 Linux is deprecated</param>
/// <param name="killArm32">Feature flag indicating ARM32 Linux should no longer work</param>
/// <returns>A tuple containing the adjusted node version and an optional warning message</returns>
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(
string preferredVersion,
bool deprecateArm32 = false,
bool killArm32 = false,
string node20RemovalDate = null)
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
{
bool isArm32Linux = Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux);
if (!isArm32Linux)
{
return (preferredVersion, null);
}
// ARM32 kill switch: runner should no longer work on this platform
if (killArm32)
{
return (null, "Linux ARM32 runners are no longer supported. Please migrate to a supported platform.");
}
// ARM32 deprecation warning: continue using node20 but warn about upcoming end of support
if (deprecateArm32)
{
string effectiveDate = string.IsNullOrEmpty(node20RemovalDate) ? Constants.Runner.NodeMigration.Node20RemovalDate : node20RemovalDate;
string deprecationWarning = string.Format(
Constants.Runner.NodeMigration.LinuxArm32DeprecationMessage,
effectiveDate);
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
{
return (Constants.Runner.NodeMigration.Node20, deprecationWarning);
}
return (preferredVersion, deprecationWarning);
}
// Legacy behavior: fall back to node20 if node24 was requested on ARM32
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) &&
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
{
return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
}

View File

@@ -141,9 +141,9 @@ namespace GitHub.Runner.Listener
}
catch (AccessDeniedException e) when (e.ErrorCode == 1)
{
terminal.WriteError($"An error occurred: {e.Message}");
terminal.WriteError($"An error occured: {e.Message}");
trace.Error(e);
return GetRunnerVersionDeprecatedExitCode();
return Constants.Runner.ReturnCode.TerminatedError;
}
catch (RunnerNotFoundException e)
{
@@ -159,16 +159,6 @@ namespace GitHub.Runner.Listener
}
}
private static int GetRunnerVersionDeprecatedExitCode()
{
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Actions.ReturnVersionDeprecatedExitCode)))
{
return Constants.Runner.ReturnCode.RunnerVersionDeprecated;
}
return Constants.Runner.ReturnCode.TerminatedError;
}
private static void LoadAndSetEnv()
{
var binDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);

View File

@@ -115,14 +115,6 @@ namespace GitHub.Runner.Worker
executionContext.Result = TaskResult.Failed;
throw;
}
catch (FailedToDownloadActionException ex)
{
// Log the error and fail the PrepareActionsAsync Initialization.
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
executionContext.InfrastructureError(ex.InnerException?.Message ?? ex.Message, category: "error_download_action");
executionContext.Result = TaskResult.Failed;
throw;
}
catch (InvalidActionArchiveException ex)
{
// Log the error and fail the PrepareActionsAsync Initialization.
@@ -1165,101 +1157,92 @@ namespace GitHub.Runner.Worker
// Allow up to 20 * 60s for any action to be downloaded from github graph.
int timeoutSeconds = 20 * 60;
try
while (retryCount < 3)
{
while (retryCount < 3)
string requestId = string.Empty;
using (var actionDownloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
using (var actionDownloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(actionDownloadTimeout.Token, executionContext.CancellationToken))
{
string requestId = string.Empty;
using (var actionDownloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
using (var actionDownloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(actionDownloadTimeout.Token, executionContext.CancellationToken))
try
{
try
//open zip stream in async mode
using (FileStream fs = new(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: _defaultFileStreamBufferSize, useAsync: true))
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
using (var httpClient = new HttpClient(httpClientHandler))
{
//open zip stream in async mode
using (FileStream fs = new(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: _defaultFileStreamBufferSize, useAsync: true))
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
using (var httpClient = new HttpClient(httpClientHandler))
httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadAuthToken);
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
using (var response = await httpClient.GetAsync(downloadUrl))
{
httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadAuthToken);
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
using (var response = await httpClient.GetAsync(downloadUrl))
requestId = UrlUtil.GetGitHubRequestId(response.Headers);
if (!string.IsNullOrEmpty(requestId))
{
requestId = UrlUtil.GetGitHubRequestId(response.Headers);
if (!string.IsNullOrEmpty(requestId))
{
Trace.Info($"Request URL: {downloadUrl} X-GitHub-Request-Id: {requestId} Http Status: {response.StatusCode}");
}
Trace.Info($"Request URL: {downloadUrl} X-GitHub-Request-Id: {requestId} Http Status: {response.StatusCode}");
}
if (response.IsSuccessStatusCode)
if (response.IsSuccessStatusCode)
{
using (var result = await response.Content.ReadAsStreamAsync())
{
using (var result = await response.Content.ReadAsStreamAsync())
{
await result.CopyToAsync(fs, _defaultCopyBufferSize, actionDownloadCancellation.Token);
await fs.FlushAsync(actionDownloadCancellation.Token);
await result.CopyToAsync(fs, _defaultCopyBufferSize, actionDownloadCancellation.Token);
await fs.FlushAsync(actionDownloadCancellation.Token);
// download succeed, break out the retry loop.
break;
}
}
else if (response.StatusCode == HttpStatusCode.NotFound)
{
// It doesn't make sense to retry in this case, so just stop
throw new ActionNotFoundException(new Uri(downloadUrl), requestId);
}
else
{
// Something else bad happened, let's go to our retry logic
response.EnsureSuccessStatusCode();
// download succeed, break out the retry loop.
break;
}
}
else if (response.StatusCode == HttpStatusCode.NotFound)
{
// It doesn't make sense to retry in this case, so just stop
throw new ActionNotFoundException(new Uri(downloadUrl), requestId);
}
else
{
// Something else bad happened, let's go to our retry logic
response.EnsureSuccessStatusCode();
}
}
}
catch (OperationCanceledException) when (executionContext.CancellationToken.IsCancellationRequested)
{
Trace.Info("Action download has been cancelled.");
throw;
}
catch (OperationCanceledException ex) when (!executionContext.CancellationToken.IsCancellationRequested && retryCount >= 2)
{
Trace.Info($"Action download final retry timeout after {timeoutSeconds} seconds.");
throw new TimeoutException($"Action '{downloadUrl}' download has timed out. Error: {ex.Message} {requestId}");
}
catch (ActionNotFoundException)
{
Trace.Info($"The action at '{downloadUrl}' does not exist");
throw;
}
catch (Exception ex) when (retryCount < 2)
{
retryCount++;
Trace.Error($"Fail to download archive '{downloadUrl}' -- Attempt: {retryCount}");
Trace.Error(ex);
if (actionDownloadTimeout.Token.IsCancellationRequested)
{
// action download didn't finish within timeout
executionContext.Warning($"Action '{downloadUrl}' didn't finish download within {timeoutSeconds} seconds. {requestId}");
}
else
{
executionContext.Warning($"Failed to download action '{downloadUrl}'. Error: {ex.Message} {requestId}");
}
}
}
if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF")))
catch (OperationCanceledException) when (executionContext.CancellationToken.IsCancellationRequested)
{
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
executionContext.Warning($"Back off {backOff.TotalSeconds} seconds before retry.");
await Task.Delay(backOff);
Trace.Info("Action download has been cancelled.");
throw;
}
catch (OperationCanceledException ex) when (!executionContext.CancellationToken.IsCancellationRequested && retryCount >= 2)
{
Trace.Info($"Action download final retry timeout after {timeoutSeconds} seconds.");
throw new TimeoutException($"Action '{downloadUrl}' download has timed out. Error: {ex.Message} {requestId}");
}
catch (ActionNotFoundException)
{
Trace.Info($"The action at '{downloadUrl}' does not exist");
throw;
}
catch (Exception ex) when (retryCount < 2)
{
retryCount++;
Trace.Error($"Fail to download archive '{downloadUrl}' -- Attempt: {retryCount}");
Trace.Error(ex);
if (actionDownloadTimeout.Token.IsCancellationRequested)
{
// action download didn't finish within timeout
executionContext.Warning($"Action '{downloadUrl}' didn't finish download within {timeoutSeconds} seconds. {requestId}");
}
else
{
executionContext.Warning($"Failed to download action '{downloadUrl}'. Error: {ex.Message} {requestId}");
}
}
}
}
catch (Exception ex) when (!(ex is OperationCanceledException) && !executionContext.CancellationToken.IsCancellationRequested)
{
Trace.Error($"Failed to download archive '{downloadUrl}' after {retryCount + 1} attempts.");
Trace.Error(ex);
throw new FailedToDownloadActionException($"Failed to download archive '{downloadUrl}' after {retryCount + 1} attempts.", ex);
if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF")))
{
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
executionContext.Warning($"Back off {backOff.TotalSeconds} seconds before retry.");
await Task.Delay(backOff);
}
}
ArgUtil.NotNullOrEmpty(archiveFile, nameof(archiveFile));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,369 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Handlers;
namespace GitHub.Runner.Worker.Dap
{
/// <summary>
/// Executes <see cref="RunCommand"/> objects in the job's runtime context.
///
/// Mirrors the behavior of a normal workflow <c>run:</c> step as closely
/// as possible by reusing the runner's existing shell-resolution logic,
/// script fixup helpers, and process execution infrastructure.
///
/// Output is streamed to the debugger via DAP <c>output</c> events with
/// secrets masked before emission.
/// </summary>
internal sealed class DapReplExecutor
{
private readonly IHostContext _hostContext;
private readonly Action<string, string> _sendOutput;
private readonly Tracing _trace;
public DapReplExecutor(IHostContext hostContext, Action<string, string> sendOutput)
{
_hostContext = hostContext ?? throw new ArgumentNullException(nameof(hostContext));
_sendOutput = sendOutput ?? throw new ArgumentNullException(nameof(sendOutput));
_trace = hostContext.GetTrace(nameof(DapReplExecutor));
}
/// <summary>
/// Executes a <see cref="RunCommand"/> and returns the exit code as a
/// formatted <see cref="EvaluateResponseBody"/>.
/// </summary>
public async Task<EvaluateResponseBody> ExecuteRunCommandAsync(
RunCommand command,
IExecutionContext context,
CancellationToken cancellationToken)
{
if (context == null)
{
return ErrorResult("No execution context available. The debugger must be paused at a step to run commands.");
}
try
{
return await ExecuteScriptAsync(command, context, cancellationToken);
}
catch (Exception ex)
{
_trace.Error($"REPL run command failed ({ex.GetType().Name})");
var maskedError = _hostContext.SecretMasker.MaskSecrets(ex.Message);
return ErrorResult($"Command failed: {maskedError}");
}
}
private async Task<EvaluateResponseBody> ExecuteScriptAsync(
RunCommand command,
IExecutionContext context,
CancellationToken cancellationToken)
{
// 1. Resolve shell — same logic as ScriptHandler
string shellCommand;
string argFormat;
if (!string.IsNullOrEmpty(command.Shell))
{
// Explicit shell from the DSL
var parsed = ScriptHandlerHelpers.ParseShellOptionString(command.Shell);
shellCommand = parsed.shellCommand;
argFormat = string.IsNullOrEmpty(parsed.shellArgs)
? ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand)
: parsed.shellArgs;
}
else
{
// Default shell — mirrors ScriptHandler platform defaults
shellCommand = ResolveDefaultShell(context);
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
_trace.Info("Resolved REPL shell");
// 2. Expand ${{ }} expressions in the script body, just like
// ActionRunner evaluates step inputs before ScriptHandler sees them
var contents = ExpandExpressions(command.Script, context);
contents = ScriptHandlerHelpers.FixUpScriptContents(shellCommand, contents);
// Write to a temp file (same pattern as ScriptHandler)
var extension = ScriptHandlerHelpers.GetScriptFileExtension(shellCommand);
var scriptFilePath = Path.Combine(
_hostContext.GetDirectory(WellKnownDirectory.Temp),
$"dap_repl_{Guid.NewGuid()}{extension}");
Encoding encoding = new UTF8Encoding(false);
#if OS_WINDOWS
contents = contents.Replace("\r\n", "\n").Replace("\n", "\r\n");
encoding = Console.InputEncoding.CodePage != 65001
? Console.InputEncoding
: encoding;
#endif
File.WriteAllText(scriptFilePath, contents, encoding);
try
{
// 3. Format arguments with script path
var resolvedPath = scriptFilePath.Replace("\"", "\\\"");
if (string.IsNullOrEmpty(argFormat) || !argFormat.Contains("{0}"))
{
return ErrorResult($"Invalid shell option '{shellCommand}'. Shell must be a valid built-in (bash, sh, cmd, powershell, pwsh) or a format string containing '{{0}}'");
}
var arguments = string.Format(argFormat, resolvedPath);
// 4. Resolve shell command path
string prependPath = string.Join(
Path.PathSeparator.ToString(),
Enumerable.Reverse(context.Global.PrependPath));
var commandPath = WhichUtil.Which(shellCommand, false, _trace, prependPath)
?? shellCommand;
// 5. Build environment — merge from execution context like a real step
var environment = BuildEnvironment(context, command.Env);
// 6. Resolve working directory
var workingDirectory = command.WorkingDirectory;
if (string.IsNullOrEmpty(workingDirectory))
{
var githubContext = context.ExpressionValues.TryGetValue("github", out var gh)
? gh as DictionaryContextData
: null;
var workspace = githubContext?.TryGetValue("workspace", out var ws) == true
? (ws as StringContextData)?.Value
: null;
workingDirectory = workspace ?? _hostContext.GetDirectory(WellKnownDirectory.Work);
}
_trace.Info("Executing REPL command");
// Stream execution info to debugger
SendOutput("console", $"$ {shellCommand} {command.Script.Substring(0, Math.Min(command.Script.Length, 80))}{(command.Script.Length > 80 ? "..." : "")}\n");
// 7. Execute via IProcessInvoker (same as DefaultStepHost)
int exitCode;
using (var processInvoker = _hostContext.CreateService<IProcessInvoker>())
{
processInvoker.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
var masked = _hostContext.SecretMasker.MaskSecrets(args.Data);
SendOutput("stdout", masked + "\n");
}
};
processInvoker.ErrorDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
var masked = _hostContext.SecretMasker.MaskSecrets(args.Data);
SendOutput("stderr", masked + "\n");
}
};
exitCode = await processInvoker.ExecuteAsync(
workingDirectory: workingDirectory,
fileName: commandPath,
arguments: arguments,
environment: environment,
requireExitCodeZero: false,
outputEncoding: null,
killProcessOnCancel: true,
cancellationToken: cancellationToken);
}
_trace.Info($"REPL command exited with code {exitCode}");
// 8. Return only the exit code summary (output was already streamed)
return new EvaluateResponseBody
{
Result = exitCode == 0 ? $"(exit code: {exitCode})" : $"Process completed with exit code {exitCode}.",
Type = exitCode == 0 ? "string" : "error",
VariablesReference = 0
};
}
finally
{
// Clean up temp script file
try { File.Delete(scriptFilePath); }
catch { /* best effort */ }
}
}
/// <summary>
/// Expands <c>${{ }}</c> expressions in the input string using the
/// runner's template evaluator — the same evaluation path that processes
/// step inputs before <see cref="ScriptHandler"/> runs them.
///
/// Each <c>${{ expr }}</c> occurrence is individually evaluated and
/// replaced with its masked string result, mirroring the semantics of
/// expression interpolation in a workflow <c>run:</c> step body.
/// </summary>
internal string ExpandExpressions(string input, IExecutionContext context)
{
if (string.IsNullOrEmpty(input) || !input.Contains("${{"))
{
return input ?? string.Empty;
}
var result = new StringBuilder();
int pos = 0;
while (pos < input.Length)
{
var start = input.IndexOf("${{", pos, StringComparison.Ordinal);
if (start < 0)
{
result.Append(input, pos, input.Length - pos);
break;
}
// Append the literal text before the expression
result.Append(input, pos, start - pos);
var end = input.IndexOf("}}", start + 3, StringComparison.Ordinal);
if (end < 0)
{
// Unterminated expression — keep literal
result.Append(input, start, input.Length - start);
break;
}
var expr = input.Substring(start + 3, end - start - 3).Trim();
end += 2; // skip past "}}"
// Evaluate the expression
try
{
var templateEvaluator = context.ToPipelineTemplateEvaluator();
var token = new GitHub.DistributedTask.ObjectTemplating.Tokens.BasicExpressionToken(
null, null, null, expr);
var evaluated = templateEvaluator.EvaluateStepDisplayName(
token,
context.ExpressionValues,
context.ExpressionFunctions);
result.Append(_hostContext.SecretMasker.MaskSecrets(evaluated ?? string.Empty));
}
catch (Exception ex)
{
_trace.Warning($"Expression expansion failed ({ex.GetType().Name})");
// Keep the original expression literal on failure
result.Append(input, start, end - start);
}
pos = end;
}
return result.ToString();
}
/// <summary>
/// Resolves the default shell the same way <see cref="ScriptHandler"/>
/// does: check job defaults, then fall back to platform default.
/// </summary>
internal string ResolveDefaultShell(IExecutionContext context)
{
// Check job defaults
if (context.Global?.JobDefaults != null &&
context.Global.JobDefaults.TryGetValue("run", out var runDefaults) &&
runDefaults.TryGetValue("shell", out var defaultShell) &&
!string.IsNullOrEmpty(defaultShell))
{
_trace.Info("Using job default shell");
return defaultShell;
}
#if OS_WINDOWS
string prependPath = string.Join(
Path.PathSeparator.ToString(),
context.Global?.PrependPath != null ? Enumerable.Reverse(context.Global.PrependPath) : Array.Empty<string>());
var pwshPath = WhichUtil.Which("pwsh", false, _trace, prependPath);
return !string.IsNullOrEmpty(pwshPath) ? "pwsh" : "powershell";
#else
return "sh";
#endif
}
/// <summary>
/// Merges the job context environment with any REPL-specific overrides.
/// </summary>
internal Dictionary<string, string> BuildEnvironment(
IExecutionContext context,
Dictionary<string, string> replEnv)
{
var env = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer);
// Pull environment from the execution context (same as ActionRunner)
if (context.ExpressionValues.TryGetValue("env", out var envData))
{
if (envData is DictionaryContextData dictEnv)
{
foreach (var pair in dictEnv)
{
if (pair.Value is StringContextData str)
{
env[pair.Key] = str.Value;
}
}
}
else if (envData is CaseSensitiveDictionaryContextData csEnv)
{
foreach (var pair in csEnv)
{
if (pair.Value is StringContextData str)
{
env[pair.Key] = str.Value;
}
}
}
}
// Expose runtime context variables to the environment (GITHUB_*, RUNNER_*, etc.)
foreach (var ctxPair in context.ExpressionValues)
{
if (ctxPair.Value is IEnvironmentContextData runtimeContext && runtimeContext != null)
{
foreach (var rtEnv in runtimeContext.GetRuntimeEnvironmentVariables())
{
env[rtEnv.Key] = rtEnv.Value;
}
}
}
// Apply REPL-specific overrides last (so they win),
// expanding any ${{ }} expressions in the values
if (replEnv != null)
{
foreach (var pair in replEnv)
{
env[pair.Key] = ExpandExpressions(pair.Value, context);
}
}
return env;
}
private void SendOutput(string category, string text)
{
_sendOutput(category, text);
}
private static EvaluateResponseBody ErrorResult(string message)
{
return new EvaluateResponseBody
{
Result = message,
Type = "error",
VariablesReference = 0
};
}
}
}

View File

@@ -0,0 +1,411 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace GitHub.Runner.Worker.Dap
{
/// <summary>
/// Base type for all REPL DSL commands.
/// </summary>
internal abstract class DapReplCommand
{
}
/// <summary>
/// <c>help</c> or <c>help("run")</c>
/// </summary>
internal sealed class HelpCommand : DapReplCommand
{
public string Topic { get; set; }
}
/// <summary>
/// <c>run("echo hello")</c> or
/// <c>run("echo hello", shell: "bash", env: { FOO: "bar" }, working_directory: "/tmp")</c>
/// </summary>
internal sealed class RunCommand : DapReplCommand
{
public string Script { get; set; }
public string Shell { get; set; }
public Dictionary<string, string> Env { get; set; }
public string WorkingDirectory { get; set; }
}
/// <summary>
/// Parses REPL input into typed <see cref="DapReplCommand"/> objects.
///
/// Grammar (intentionally minimal — extend as the DSL grows):
/// <code>
/// help → HelpCommand { Topic = null }
/// help("run") → HelpCommand { Topic = "run" }
/// run("script body") → RunCommand { Script = "script body" }
/// run("script", shell: "bash") → RunCommand { Shell = "bash" }
/// run("script", env: { K: "V" }) → RunCommand { Env = { K → V } }
/// run("script", working_directory: "p")→ RunCommand { WorkingDirectory = "p" }
/// </code>
///
/// Parsing is intentionally hand-rolled rather than regex-based so it can
/// handle nested braces, quoted strings with escapes, and grow to support
/// future commands without accumulating regex complexity.
/// </summary>
internal static class DapReplParser
{
/// <summary>
/// Attempts to parse REPL input into a command. Returns null if the
/// input does not match any known DSL command (i.e. it should be
/// treated as an expression instead).
/// </summary>
internal static DapReplCommand TryParse(string input, out string error)
{
error = null;
if (string.IsNullOrWhiteSpace(input))
{
return null;
}
var trimmed = input.Trim();
// help / help("topic")
if (trimmed.Equals("help", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("help(", StringComparison.OrdinalIgnoreCase))
{
return ParseHelp(trimmed, out error);
}
// run("...")
if (trimmed.StartsWith("run(", StringComparison.OrdinalIgnoreCase))
{
return ParseRun(trimmed, out error);
}
// Not a DSL command
return null;
}
internal static string GetGeneralHelp()
{
return """
Actions Debug Console
Commands:
help Show this help
help("run") Show help for the run command
run("script") Execute a script (like a workflow run step)
Anything else is evaluated as a GitHub Actions expression.
Example: github.repository
Example: ${{ github.event_name }}
""";
}
internal static string GetRunHelp()
{
return """
run command execute a script in the job context
Usage:
run("echo hello")
run("echo $FOO", shell: "bash")
run("echo $FOO", env: { FOO: "bar" })
run("ls", working_directory: "/tmp")
run("echo $X", shell: "bash", env: { X: "1" }, working_directory: "/tmp")
Options:
shell: Shell to use (default: job default, e.g. bash)
env: Extra environment variables as { KEY: "value" }
working_directory: Working directory for the command
Behavior:
- Equivalent to a workflow `run:` step
- Expressions in the script body are expanded (${{ ... }})
- Output is streamed in real time and secrets are masked
""";
}
#region Parsers
private static HelpCommand ParseHelp(string input, out string error)
{
error = null;
if (input.Equals("help", StringComparison.OrdinalIgnoreCase))
{
return new HelpCommand();
}
// help("topic")
var inner = ExtractParenthesizedArgs(input, "help", out error);
if (error != null) return null;
var topic = ExtractQuotedString(inner.Trim(), out error);
if (error != null) return null;
return new HelpCommand { Topic = topic };
}
private static RunCommand ParseRun(string input, out string error)
{
error = null;
var inner = ExtractParenthesizedArgs(input, "run", out error);
if (error != null) return null;
// Split into argument list respecting quotes and braces
var args = SplitArguments(inner, out error);
if (error != null) return null;
if (args.Count == 0)
{
error = "run() requires a script argument. Example: run(\"echo hello\")";
return null;
}
// First arg must be the script body (a quoted string)
var script = ExtractQuotedString(args[0].Trim(), out error);
if (error != null)
{
error = $"First argument to run() must be a quoted string. {error}";
return null;
}
var cmd = new RunCommand { Script = script };
// Parse remaining keyword arguments
for (int i = 1; i < args.Count; i++)
{
var kv = args[i].Trim();
var colonIdx = kv.IndexOf(':');
if (colonIdx <= 0)
{
error = $"Expected keyword argument (e.g. shell: \"bash\"), got: {kv}";
return null;
}
var key = kv.Substring(0, colonIdx).Trim();
var value = kv.Substring(colonIdx + 1).Trim();
switch (key.ToLowerInvariant())
{
case "shell":
cmd.Shell = ExtractQuotedString(value, out error);
if (error != null) { error = $"shell: {error}"; return null; }
break;
case "working_directory":
cmd.WorkingDirectory = ExtractQuotedString(value, out error);
if (error != null) { error = $"working_directory: {error}"; return null; }
break;
case "env":
cmd.Env = ParseEnvBlock(value, out error);
if (error != null) { error = $"env: {error}"; return null; }
break;
default:
error = $"Unknown option: {key}. Valid options: shell, env, working_directory";
return null;
}
}
return cmd;
}
#endregion
#region Low-level parsing helpers
/// <summary>
/// Given "cmd(...)" returns the inner content between the outer parens.
/// </summary>
private static string ExtractParenthesizedArgs(string input, string prefix, out string error)
{
error = null;
var start = prefix.Length; // skip "cmd"
if (start >= input.Length || input[start] != '(')
{
error = $"Expected '(' after {prefix}";
return null;
}
if (input[input.Length - 1] != ')')
{
error = $"Expected ')' at end of {prefix}(...)";
return null;
}
return input.Substring(start + 1, input.Length - start - 2);
}
/// <summary>
/// Extracts a double-quoted string value, handling escaped quotes.
/// </summary>
internal static string ExtractQuotedString(string input, out string error)
{
error = null;
if (string.IsNullOrEmpty(input))
{
error = "Expected a quoted string, got empty input";
return null;
}
if (input[0] != '"')
{
error = $"Expected a quoted string starting with \", got: {Truncate(input, 40)}";
return null;
}
var sb = new StringBuilder();
for (int i = 1; i < input.Length; i++)
{
if (input[i] == '\\' && i + 1 < input.Length)
{
sb.Append(input[i + 1]);
i++;
}
else if (input[i] == '"')
{
// Check nothing meaningful follows the closing quote
var rest = input.Substring(i + 1).Trim();
if (rest.Length > 0)
{
error = $"Unexpected content after closing quote: {Truncate(rest, 40)}";
return null;
}
return sb.ToString();
}
else
{
sb.Append(input[i]);
}
}
error = "Unterminated string (missing closing \")";
return null;
}
/// <summary>
/// Splits a comma-separated argument list, respecting quoted strings
/// and nested braces so that <c>"a, b", env: { K: "V, W" }</c> is
/// correctly split into two arguments.
/// </summary>
internal static List<string> SplitArguments(string input, out string error)
{
error = null;
var result = new List<string>();
var current = new StringBuilder();
int depth = 0;
bool inQuote = false;
for (int i = 0; i < input.Length; i++)
{
var ch = input[i];
if (ch == '\\' && inQuote && i + 1 < input.Length)
{
current.Append(ch);
current.Append(input[++i]);
continue;
}
if (ch == '"')
{
inQuote = !inQuote;
current.Append(ch);
continue;
}
if (!inQuote)
{
if (ch == '{')
{
depth++;
current.Append(ch);
continue;
}
if (ch == '}')
{
depth--;
current.Append(ch);
continue;
}
if (ch == ',' && depth == 0)
{
result.Add(current.ToString());
current.Clear();
continue;
}
}
current.Append(ch);
}
if (inQuote)
{
error = "Unterminated string in arguments";
return null;
}
if (depth != 0)
{
error = "Unmatched braces in arguments";
return null;
}
if (current.Length > 0)
{
result.Add(current.ToString());
}
return result;
}
/// <summary>
/// Parses <c>{ KEY: "value", KEY2: "value2" }</c> into a dictionary.
/// </summary>
internal static Dictionary<string, string> ParseEnvBlock(string input, out string error)
{
error = null;
var trimmed = input.Trim();
if (!trimmed.StartsWith("{") || !trimmed.EndsWith("}"))
{
error = "Expected env block in the form { KEY: \"value\" }";
return null;
}
var inner = trimmed.Substring(1, trimmed.Length - 2).Trim();
if (string.IsNullOrEmpty(inner))
{
return new Dictionary<string, string>();
}
var pairs = SplitArguments(inner, out error);
if (error != null) return null;
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in pairs)
{
var colonIdx = pair.IndexOf(':');
if (colonIdx <= 0)
{
error = $"Expected KEY: \"value\" pair, got: {Truncate(pair.Trim(), 40)}";
return null;
}
var key = pair.Substring(0, colonIdx).Trim();
var val = ExtractQuotedString(pair.Substring(colonIdx + 1).Trim(), out error);
if (error != null) return null;
result[key] = val;
}
return result;
}
private static string Truncate(string value, int maxLength)
{
if (value == null) return "(null)";
return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "...";
}
#endregion
}
}

View File

@@ -0,0 +1,387 @@
using System;
using System.Collections.Generic;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.Runner.Common;
namespace GitHub.Runner.Worker.Dap
{
/// <summary>
/// Maps runner execution context data to DAP scopes and variables.
///
/// This is the single point where runner context values are materialized
/// for the debugger. All values pass through the runner's existing
/// <see cref="GitHub.DistributedTask.Logging.ISecretMasker"/> so the DAP
/// surface never exposes anything beyond what a normal CI log would show.
///
/// The secrets scope is intentionally opaque: keys are visible but every
/// value is replaced with a constant redaction marker.
///
/// Designed to be reusable by future DAP features (evaluate, hover, REPL)
/// so that masking policy is never duplicated.
/// </summary>
internal sealed class DapVariableProvider
{
// Well-known scope names that map to top-level expression contexts.
// Order matters: the index determines the stable variablesReference ID.
internal static readonly string[] ScopeNames =
{
"github", "env", "runner", "job", "steps",
"secrets", "inputs", "vars", "matrix", "needs"
};
// Scope references occupy the range [1, ScopeReferenceMax].
private const int ScopeReferenceBase = 1;
private const int ScopeReferenceMax = 100;
// Dynamic (nested) variable references start above the scope range.
private const int DynamicReferenceBase = 101;
internal const string RedactedValue = "***";
private readonly IHostContext _hostContext;
// Maps dynamic variable reference IDs to the backing data and its
// dot-separated path (e.g. "github.event.pull_request").
private readonly Dictionary<int, (PipelineContextData Data, string Path)> _variableReferences = new();
private int _nextVariableReference = DynamicReferenceBase;
public DapVariableProvider(IHostContext hostContext)
{
_hostContext = hostContext ?? throw new ArgumentNullException(nameof(hostContext));
}
/// <summary>
/// Clears all dynamic variable references.
/// Call this whenever the paused execution context changes (e.g. new step)
/// so that stale nested references are not served to the client.
/// </summary>
public void Reset()
{
_variableReferences.Clear();
_nextVariableReference = DynamicReferenceBase;
}
/// <summary>
/// Returns the list of DAP scopes for the given execution context.
/// Each scope corresponds to a well-known runner expression context
/// (github, env, secrets, …) and carries a stable variablesReference
/// that the client can use to drill into variables.
/// </summary>
public List<Scope> GetScopes(IExecutionContext context)
{
var scopes = new List<Scope>();
if (context?.ExpressionValues == null)
{
return scopes;
}
for (int i = 0; i < ScopeNames.Length; i++)
{
var scopeName = ScopeNames[i];
if (!context.ExpressionValues.TryGetValue(scopeName, out var value) || value == null)
{
continue;
}
var scope = new Scope
{
Name = scopeName,
VariablesReference = ScopeReferenceBase + i,
Expensive = false,
PresentationHint = scopeName == "secrets" ? "registers" : null
};
if (value is DictionaryContextData dict)
{
scope.NamedVariables = dict.Count;
}
else if (value is CaseSensitiveDictionaryContextData csDict)
{
scope.NamedVariables = csDict.Count;
}
scopes.Add(scope);
}
return scopes;
}
/// <summary>
/// Returns the child variables for a given variablesReference.
/// The reference may point at a top-level scope (1100) or a
/// dynamically registered nested container (101+).
/// </summary>
public List<Variable> GetVariables(IExecutionContext context, int variablesReference)
{
var variables = new List<Variable>();
if (context?.ExpressionValues == null)
{
return variables;
}
PipelineContextData data = null;
string basePath = null;
bool isSecretsScope = false;
if (variablesReference >= ScopeReferenceBase && variablesReference <= ScopeReferenceMax)
{
var scopeIndex = variablesReference - ScopeReferenceBase;
if (scopeIndex < ScopeNames.Length)
{
var scopeName = ScopeNames[scopeIndex];
isSecretsScope = scopeName == "secrets";
if (context.ExpressionValues.TryGetValue(scopeName, out data))
{
basePath = scopeName;
}
}
}
else if (_variableReferences.TryGetValue(variablesReference, out var refData))
{
data = refData.Data;
basePath = refData.Path;
isSecretsScope = basePath?.StartsWith("secrets", StringComparison.OrdinalIgnoreCase) == true;
}
if (data == null)
{
return variables;
}
ConvertToVariables(data, basePath, isSecretsScope, variables);
return variables;
}
/// <summary>
/// Applies the runner's secret masker to the given value.
/// This is the single masking entry-point for all DAP-visible strings
/// and is intentionally public so future DAP features (evaluate, REPL)
/// can reuse it without duplicating policy.
/// </summary>
public string MaskSecrets(string value)
{
if (string.IsNullOrEmpty(value))
{
return value ?? string.Empty;
}
return _hostContext.SecretMasker.MaskSecrets(value);
}
/// <summary>
/// Evaluates a GitHub Actions expression (e.g. "github.repository",
/// "${{ github.event_name }}") in the context of the current step and
/// returns a masked result suitable for the DAP evaluate response.
///
/// Uses the runner's standard <see cref="GitHub.DistributedTask.Pipelines.ObjectTemplating.IPipelineTemplateEvaluator"/>
/// so the full expression language is available (functions, operators,
/// context access).
/// </summary>
public EvaluateResponseBody EvaluateExpression(string expression, IExecutionContext context)
{
if (context?.ExpressionValues == null)
{
return new EvaluateResponseBody
{
Result = "(no execution context available)",
Type = "string",
VariablesReference = 0
};
}
// Strip ${{ }} wrapper if present
var expr = expression?.Trim() ?? string.Empty;
if (expr.StartsWith("${{") && expr.EndsWith("}}"))
{
expr = expr.Substring(3, expr.Length - 5).Trim();
}
if (string.IsNullOrEmpty(expr))
{
return new EvaluateResponseBody
{
Result = string.Empty,
Type = "string",
VariablesReference = 0
};
}
try
{
var templateEvaluator = context.ToPipelineTemplateEvaluator();
var token = new BasicExpressionToken(null, null, null, expr);
var result = templateEvaluator.EvaluateStepDisplayName(
token,
context.ExpressionValues,
context.ExpressionFunctions);
result = MaskSecrets(result ?? "null");
return new EvaluateResponseBody
{
Result = result,
Type = InferResultType(result),
VariablesReference = 0
};
}
catch (Exception ex)
{
var errorMessage = MaskSecrets($"Evaluation error: {ex.Message}");
return new EvaluateResponseBody
{
Result = errorMessage,
Type = "string",
VariablesReference = 0
};
}
}
/// <summary>
/// Infers a simple DAP type hint from the string representation of a result.
/// </summary>
internal static string InferResultType(string value)
{
if (value == null || value == "null")
return "null";
if (value == "true" || value == "false")
return "boolean";
if (double.TryParse(value, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out _))
return "number";
if (value.StartsWith("{") || value.StartsWith("["))
return "object";
return "string";
}
#region Private helpers
private void ConvertToVariables(
PipelineContextData data,
string basePath,
bool isSecretsScope,
List<Variable> variables)
{
switch (data)
{
case DictionaryContextData dict:
foreach (var pair in dict)
{
variables.Add(CreateVariable(pair.Key, pair.Value, basePath, isSecretsScope));
}
break;
case CaseSensitiveDictionaryContextData csDict:
foreach (var pair in csDict)
{
variables.Add(CreateVariable(pair.Key, pair.Value, basePath, isSecretsScope));
}
break;
case ArrayContextData array:
for (int i = 0; i < array.Count; i++)
{
var variable = CreateVariable($"[{i}]", array[i], basePath, isSecretsScope);
variables.Add(variable);
}
break;
}
}
private Variable CreateVariable(
string name,
PipelineContextData value,
string basePath,
bool isSecretsScope)
{
var childPath = string.IsNullOrEmpty(basePath) ? name : $"{basePath}.{name}";
var variable = new Variable
{
Name = name,
EvaluateName = $"${{{{ {childPath} }}}}"
};
if (value == null)
{
variable.Value = isSecretsScope ? RedactedValue : "null";
variable.Type = "null";
variable.VariablesReference = 0;
return variable;
}
// Secrets scope: redact ALL values regardless of underlying type.
// Keys are visible but values are always replaced with the
// redaction marker, and nested containers are not drillable.
if (isSecretsScope)
{
variable.Value = RedactedValue;
variable.Type = "string";
variable.VariablesReference = 0;
return variable;
}
switch (value)
{
case StringContextData str:
variable.Value = MaskSecrets(str.Value);
variable.Type = "string";
variable.VariablesReference = 0;
break;
case NumberContextData num:
variable.Value = MaskSecrets(num.Value.ToString("G15", System.Globalization.CultureInfo.InvariantCulture));
variable.Type = "number";
variable.VariablesReference = 0;
break;
case BooleanContextData boolVal:
variable.Value = MaskSecrets(boolVal.Value ? "true" : "false");
variable.Type = "boolean";
variable.VariablesReference = 0;
break;
case DictionaryContextData dict:
variable.Value = $"Object ({dict.Count} properties)";
variable.Type = "object";
variable.VariablesReference = RegisterVariableReference(dict, childPath);
variable.NamedVariables = dict.Count;
break;
case CaseSensitiveDictionaryContextData csDict:
variable.Value = $"Object ({csDict.Count} properties)";
variable.Type = "object";
variable.VariablesReference = RegisterVariableReference(csDict, childPath);
variable.NamedVariables = csDict.Count;
break;
case ArrayContextData array:
variable.Value = $"Array ({array.Count} items)";
variable.Type = "array";
variable.VariablesReference = RegisterVariableReference(array, childPath);
variable.IndexedVariables = array.Count;
break;
default:
var rawValue = value.ToJToken()?.ToString() ?? "unknown";
variable.Value = MaskSecrets(rawValue);
variable.Type = value.GetType().Name;
variable.VariablesReference = 0;
break;
}
return variable;
}
private int RegisterVariableReference(PipelineContextData data, string path)
{
var reference = _nextVariableReference++;
_variableReferences[reference] = (data, path);
return reference;
}
#endregion
}
}

View File

@@ -0,0 +1,25 @@
using System.Threading.Tasks;
using GitHub.Runner.Common;
namespace GitHub.Runner.Worker.Dap
{
public enum DapSessionState
{
WaitingForConnection,
Initializing,
Ready,
Paused,
Running,
Terminated
}
[ServiceLocator(Default = typeof(DapDebugger))]
public interface IDapDebugger : IRunnerService
{
Task StartAsync(IExecutionContext jobContext);
Task WaitUntilReadyAsync();
Task OnStepStartingAsync(IStep step);
void OnStepCompleted(IStep step);
Task OnJobCompletedAsync();
}
}

View File

@@ -77,7 +77,8 @@ namespace GitHub.Runner.Worker
List<string> StepEnvironmentOverrides { get; }
IExecutionContext Root { get; }
ExecutionContext Root { get; }
ExecutionContext Parent { get; }
// Initialize
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
@@ -250,9 +251,7 @@ namespace GitHub.Runner.Worker
}
}
IExecutionContext IExecutionContext.Root => Root;
private ExecutionContext Root
public ExecutionContext Root
{
get
{
@@ -267,7 +266,13 @@ namespace GitHub.Runner.Worker
}
}
public ExecutionContext Parent
{
get
{
return _parentExecutionContext;
}
}
public JobContext JobContext
{
@@ -854,12 +859,6 @@ namespace GitHub.Runner.Worker
// Track Node.js 20 actions for deprecation warning
Global.DeprecatedNode20Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Track actions upgraded from Node.js 20 to Node.js 24
Global.UpgradedToNode24Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Track actions stuck on Node.js 20 due to ARM32 (separate from general deprecation)
Global.Arm32Node20Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Job Outputs
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
@@ -969,6 +968,9 @@ namespace GitHub.Runner.Worker
// Verbosity (from GitHub.Step_Debug).
Global.WriteDebug = Global.Variables.Step_Debug ?? false;
// Debugger enabled flag (from acquire response).
Global.EnableDebugger = message.EnableDebugger;
// Hook up JobServerQueueThrottling event, we will log warning on server tarpit.
_jobServerQueue.JobServerQueueThrottling += JobServerQueueThrottling_EventReceived;
}

View File

@@ -27,6 +27,7 @@ namespace GitHub.Runner.Worker
public StepsContext StepsContext { get; set; }
public Variables Variables { get; set; }
public bool WriteDebug { get; set; }
public bool EnableDebugger { get; set; }
public string InfrastructureFailureCategory { get; set; }
public JObject ContainerHookState { get; set; }
public bool HasTemplateEvaluatorMismatch { get; set; }
@@ -34,7 +35,5 @@ namespace GitHub.Runner.Worker
public bool HasDeprecatedSetOutput { get; set; }
public bool HasDeprecatedSaveState { get; set; }
public HashSet<string> DeprecatedNode20Actions { get; set; }
public HashSet<string> UpgradedToNode24Actions { get; set; }
public HashSet<string> Arm32Node20Actions { get; set; }
}
}

View File

@@ -25,14 +25,6 @@ namespace GitHub.Runner.Worker.Handlers
public sealed class HandlerFactory : RunnerService, IHandlerFactory
{
internal static bool ShouldTrackAsArm32Node20(bool deprecateArm32, string preferredNodeVersion, string finalNodeVersion, string platformWarningMessage)
{
return deprecateArm32 &&
!string.IsNullOrEmpty(platformWarningMessage) &&
string.Equals(preferredNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) &&
string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.OrdinalIgnoreCase);
}
public IHandler Create(
IExecutionContext executionContext,
Pipelines.ActionStepDefinitionReference action,
@@ -73,12 +65,19 @@ namespace GitHub.Runner.Worker.Handlers
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20;
}
// Read flags early; actionName is also resolved up front for tracking after version is determined
bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false;
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
string actionName = GetActionName(action);
// Track Node.js 20 actions for deprecation annotation
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
{
bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false;
if (warnOnNode20)
{
string actionName = GetActionName(action);
if (!string.IsNullOrEmpty(actionName))
{
executionContext.Global.DeprecatedNode20Actions?.Add(actionName);
}
}
}
// Check if node20 was explicitly specified in the action
// We don't modify if node24 was explicitly specified
@@ -88,15 +87,7 @@ namespace GitHub.Runner.Worker.Handlers
bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false;
var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24);
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32, node20RemovalDate);
// ARM32 kill switch: fail the step
if (finalNodeVersion == null)
{
executionContext.Error(platformWarningMessage);
throw new InvalidOperationException(platformWarningMessage);
}
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion);
nodeData.NodeVersion = finalNodeVersion;
if (!string.IsNullOrEmpty(configWarningMessage))
@@ -109,26 +100,6 @@ namespace GitHub.Runner.Worker.Handlers
executionContext.Warning(platformWarningMessage);
}
// Track actions based on their final node version
if (!string.IsNullOrEmpty(actionName))
{
if (string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
{
// Action was upgraded from node20 to node24
executionContext.Global.UpgradedToNode24Actions?.Add(actionName);
}
else if (ShouldTrackAsArm32Node20(deprecateArm32, nodeVersion, finalNodeVersion, platformWarningMessage))
{
// Action is on node20 because ARM32 can't run node24
executionContext.Global.Arm32Node20Actions?.Add(actionName);
}
else if (warnOnNode20)
{
// Action is still running on node20 (general case)
executionContext.Global.DeprecatedNode20Actions?.Add(actionName);
}
}
// Show information about Node 24 migration in Phase 2
if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
{
@@ -138,30 +109,6 @@ namespace GitHub.Runner.Worker.Handlers
executionContext.Output(infoMessage);
}
}
else if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.InvariantCultureIgnoreCase))
{
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeData.NodeVersion, deprecateArm32, killArm32, node20RemovalDate);
// ARM32 kill switch: fail the step
if (finalNodeVersion == null)
{
executionContext.Error(platformWarningMessage);
throw new InvalidOperationException(platformWarningMessage);
}
var preferredVersion = nodeData.NodeVersion;
nodeData.NodeVersion = finalNodeVersion;
if (!string.IsNullOrEmpty(platformWarningMessage))
{
executionContext.Warning(platformWarningMessage);
}
if (!string.IsNullOrEmpty(actionName) && ShouldTrackAsArm32Node20(deprecateArm32, preferredVersion, finalNodeVersion, platformWarningMessage))
{
executionContext.Global.Arm32Node20Actions?.Add(actionName);
}
}
(handler as INodeScriptActionHandler).Data = nodeData;
}

View File

@@ -58,23 +58,13 @@ namespace GitHub.Runner.Worker.Handlers
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
{
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);
if (nodeVersion == null)
{
executionContext.Error(warningMessage);
throw new InvalidOperationException(warningMessage);
}
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
}
return Task.FromResult(nodeVersion);
}
@@ -152,18 +142,8 @@ namespace GitHub.Runner.Worker.Handlers
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
{
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);
if (nodeExternal == null)
{
executionContext.Error(warningMessage);
throw new InvalidOperationException(warningMessage);
}
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);
@@ -293,18 +273,8 @@ namespace GitHub.Runner.Worker.Handlers
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
{
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
string node20RemovalDate = executionContext.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32, killArm32, node20RemovalDate);
if (nodeExternal == null)
{
executionContext.Error(warningMessage);
throw new InvalidOperationException(warningMessage);
}
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
if (!string.IsNullOrEmpty(warningMessage))
{
executionContext.Warning(warningMessage);

View File

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

View File

@@ -736,38 +736,14 @@ namespace GitHub.Runner.Worker
}
}
// Read dates from server variables with hardcoded fallbacks
var node24DefaultDateRaw = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node24DefaultDateVariable);
var node24DefaultDate = string.IsNullOrEmpty(node24DefaultDateRaw) ? Constants.Runner.NodeMigration.Node24DefaultDate : node24DefaultDateRaw;
var node20RemovalDateRaw = context.Global.Variables?.Get(Constants.Runner.NodeMigration.Node20RemovalDateVariable);
var node20RemovalDate = string.IsNullOrEmpty(node20RemovalDateRaw) ? Constants.Runner.NodeMigration.Node20RemovalDate : node20RemovalDateRaw;
// Add deprecation warning annotation for Node.js 20 actions (Phase 1 - actions still running on node20)
// Add deprecation warning annotation for Node.js 20 actions
if (context.Global.DeprecatedNode20Actions?.Count > 0)
{
var sortedActions = context.Global.DeprecatedNode20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase);
var actionsList = string.Join(", ", sortedActions);
var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting {node24DefaultDate}. Node.js 20 will be removed from the runner on {node20RemovalDate}. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
context.Warning(deprecationMessage);
}
// Add annotation for actions upgraded from Node.js 20 to Node.js 24 (Phase 2/3)
if (context.Global.UpgradedToNode24Actions?.Count > 0)
{
var sortedActions = context.Global.UpgradedToNode24Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase);
var actionsList = string.Join(", ", sortedActions);
var upgradeMessage = $"Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: {actionsList}. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
context.Warning(upgradeMessage);
}
// Add annotation for ARM32 actions stuck on Node.js 20 (ARM32 can't run node24)
if (context.Global.Arm32Node20Actions?.Count > 0)
{
var sortedActions = context.Global.Arm32Node20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase);
var actionsList = string.Join(", ", sortedActions);
var arm32Message = $"The following actions are running on Node.js 20 because Node.js 24 is not available on Linux ARM32: {actionsList}. Linux ARM32 runners are deprecated and will no longer be supported after {node20RemovalDate}. Please migrate to a supported platform. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
context.Warning(arm32Message);
}
}
catch (Exception ex)
{

View File

@@ -13,6 +13,7 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Dap;
using GitHub.Services.Common;
using GitHub.Services.WebApi;
using Sdk.RSWebApi.Contracts;
@@ -28,6 +29,7 @@ namespace GitHub.Runner.Worker
public sealed class JobRunner : RunnerService, IJobRunner
{
private const string DebuggerConnectionTelemetryPrefix = "DebuggerConnectionResult";
private IJobServerQueue _jobServerQueue;
private RunnerSettings _runnerSettings;
private ITempDirectoryManager _tempDirectoryManager;
@@ -112,6 +114,7 @@ namespace GitHub.Runner.Worker
IExecutionContext jobContext = null;
CancellationTokenRegistration? runnerShutdownRegistration = null;
IDapDebugger dapDebugger = null;
try
{
// Create the job execution context.
@@ -178,6 +181,26 @@ namespace GitHub.Runner.Worker
_tempDirectoryManager = HostContext.GetService<ITempDirectoryManager>();
_tempDirectoryManager.InitializeTempDirectory(jobContext);
// Setup the debugger
if (jobContext.Global.EnableDebugger)
{
Trace.Info("Debugger enabled for this job run");
try
{
dapDebugger = HostContext.GetService<IDapDebugger>();
await dapDebugger.StartAsync(jobContext);
}
catch (Exception ex)
{
Trace.Error($"Failed to start DAP debugger: {ex.Message}");
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.Message}");
jobContext.Error("Failed to start debugger.");
return await CompleteJobAsync(server, jobContext, message, TaskResult.Failed);
}
}
// Get the job extension.
Trace.Info("Getting job extension.");
IJobExtension jobExtension = HostContext.CreateService<IJobExtension>();
@@ -219,6 +242,33 @@ namespace GitHub.Runner.Worker
await Task.WhenAny(_jobServerQueue.JobRecordUpdated.Task, Task.Delay(1000));
}
// Wait for DAP debugger client connection and handshake after "Set up job"
// so the job page shows the setup step before we block on the debugger
if (dapDebugger != null)
{
try
{
await dapDebugger.WaitUntilReadyAsync();
AddDebuggerConnectionTelemetry(jobContext, "Connected");
}
catch (OperationCanceledException) when (jobRequestCancellationToken.IsCancellationRequested)
{
Trace.Info("Job was cancelled before debugger client connected.");
AddDebuggerConnectionTelemetry(jobContext, "Canceled");
jobContext.Error("Job was cancelled before debugger client connected.");
return await CompleteJobAsync(server, jobContext, message, TaskResult.Canceled);
}
catch (Exception ex)
{
Trace.Error($"DAP debugger failed to become ready: {ex.Message}");
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.Message}");
// If debugging was requested but the debugger is not available, fail the job
jobContext.Error("The debugger failed to start or no debugger client connected in time.");
return await CompleteJobAsync(server, jobContext, message, TaskResult.Failed);
}
}
// Run all job steps
Trace.Info("Run all job steps.");
var stepsRunner = HostContext.GetService<IStepsRunner>();
@@ -259,6 +309,11 @@ namespace GitHub.Runner.Worker
runnerShutdownRegistration = null;
}
if (dapDebugger != null)
{
await dapDebugger.OnJobCompletedAsync();
}
await ShutdownQueue(throwOnFailure: false);
}
}
@@ -440,6 +495,15 @@ namespace GitHub.Runner.Worker
throw new AggregateException(exceptions);
}
private static void AddDebuggerConnectionTelemetry(IExecutionContext jobContext, string result)
{
jobContext.Global.JobTelemetry.Add(new JobTelemetry
{
Type = JobTelemetryType.General,
Message = $"{DebuggerConnectionTelemetryPrefix}: {result}"
});
}
private void MaskTelemetrySecrets(List<JobTelemetry> jobTelemetry)
{
foreach (var telemetryItem in jobTelemetry)

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using GitHub.Actions.WorkflowParser;
using GitHub.DistributedTask.Expressions2;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
@@ -227,12 +226,8 @@ namespace GitHub.Runner.Worker
Func<TNew> newEvaluator,
Func<TLegacy, TNew, bool> resultComparer)
{
// Use the root (job-level) cancellation token to detect cancellation race conditions.
// The step-level token only fires on step timeout, not on job cancellation.
// Job cancellation mutates JobContext.Status which expression functions read,
// so we need the root token to properly detect cancellation between evaluator runs.
var rootCancellationToken = _context.Root?.CancellationToken ?? CancellationToken.None;
var cancellationRequestedBefore = rootCancellationToken.IsCancellationRequested;
// Capture cancellation state before evaluation
var cancellationRequestedBefore = _context.CancellationToken.IsCancellationRequested;
// Legacy evaluator
var legacyException = default(Exception);
@@ -266,7 +261,7 @@ namespace GitHub.Runner.Worker
}
// Capture cancellation state after evaluation
var cancellationRequestedAfter = rootCancellationToken.IsCancellationRequested;
var cancellationRequestedAfter = _context.CancellationToken.IsCancellationRequested;
// Compare results or exceptions
bool hasMismatch = false;

View File

@@ -10,6 +10,7 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Dap;
using GitHub.Runner.Worker.Expressions;
namespace GitHub.Runner.Worker
@@ -50,6 +51,7 @@ namespace GitHub.Runner.Worker
jobContext.JobContext.Status = (jobContext.Result ?? TaskResult.Succeeded).ToActionResult();
var scopeInputs = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
bool checkPostJobActions = false;
var dapDebugger = HostContext.GetService<IDapDebugger>();
while (jobContext.JobSteps.Count > 0 || !checkPostJobActions)
{
if (jobContext.JobSteps.Count == 0 && !checkPostJobActions)
@@ -226,9 +228,14 @@ namespace GitHub.Runner.Worker
}
else
{
// Pause for DAP debugger before step execution
await dapDebugger?.OnStepStartingAsync(step);
// Run the step
await RunStepAsync(step, jobContext.CancellationToken);
CompleteStep(step);
dapDebugger?.OnStepCompleted(step);
}
}
finally
@@ -255,6 +262,7 @@ namespace GitHub.Runner.Worker
Trace.Info($"Current state: job state = '{jobContext.Result}'");
}
}
private async Task RunStepAsync(IStep step, CancellationToken jobCancellationToken)

View File

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

View File

@@ -253,6 +253,13 @@ namespace GitHub.DistributedTask.Pipelines
set;
}
[DataMember(EmitDefaultValue = false)]
public bool EnableDebugger
{
get;
set;
}
/// <summary>
/// Gets the collection of variables associated with the current context.
/// </summary>

View File

@@ -2556,25 +2556,6 @@ namespace GitHub.DistributedTask.WebApi
}
}
[Serializable]
public sealed class FailedToDownloadActionException : DistributedTaskException
{
public FailedToDownloadActionException(String message)
: base(message)
{
}
public FailedToDownloadActionException(String message, Exception innerException)
: base(message, innerException)
{
}
private FailedToDownloadActionException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
[Serializable]
public sealed class InvalidActionArchiveException : DistributedTaskException
{

View File

@@ -1,104 +0,0 @@
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

@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using Xunit;
using GitHub.DistributedTask.Pipelines;
namespace GitHub.Actions.RunService.WebApi.Tests;
public sealed class AgentJobRequestMessageL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyEnableDebuggerDeserialization_WithTrue()
{
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string jsonWithEnabledDebugger = DoubleQuotify("{'EnableDebugger': true}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(jsonWithEnabledDebugger));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.True(recoveredMessage.EnableDebugger, "EnableDebugger should be true when JSON contains 'EnableDebugger': true");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyEnableDebuggerDeserialization_DefaultToFalse()
{
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string jsonWithoutDebugger = DoubleQuotify("{'messageType': 'PipelineAgentJobRequest'}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(jsonWithoutDebugger));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.False(recoveredMessage.EnableDebugger, "EnableDebugger should default to false when JSON field is absent");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyEnableDebuggerDeserialization_WithFalse()
{
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string jsonWithDisabledDebugger = DoubleQuotify("{'EnableDebugger': false}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(jsonWithDisabledDebugger));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.False(recoveredMessage.EnableDebugger, "EnableDebugger should be false when JSON contains 'EnableDebugger': false");
}
private static string DoubleQuotify(string text)
{
return text.Replace('\'', '"');
}
}

View File

@@ -2,6 +2,7 @@
using GitHub.Runner.Listener.Check;
using GitHub.Runner.Listener.Configuration;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Dap;
using GitHub.Runner.Worker.Container.ContainerHooks;
using GitHub.Runner.Worker.Handlers;
using System;

View File

@@ -198,8 +198,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Func<Task> action = async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions);
//Assert
var ex = await Assert.ThrowsAsync<FailedToDownloadActionException>(action);
Assert.IsType<ActionNotFoundException>(ex.InnerException);
await Assert.ThrowsAsync<ActionNotFoundException>(action);
var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "main.completed");
Assert.False(File.Exists(watermarkFile));

View File

@@ -928,58 +928,6 @@ 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 = "")
{
_ecTokenSource?.Dispose();

View File

@@ -926,58 +926,6 @@ 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 = "")
{
_ecTokenSource?.Dispose();

View File

@@ -379,40 +379,6 @@ 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)
{
var messages = new List<string>();

View File

@@ -0,0 +1,500 @@
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Dap;
using Newtonsoft.Json;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class DapDebuggerL0
{
private const string PortEnvironmentVariable = "ACTIONS_RUNNER_DAP_PORT";
private const string TimeoutEnvironmentVariable = "ACTIONS_RUNNER_DAP_CONNECTION_TIMEOUT";
private DapDebugger _debugger;
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
{
var hc = new TestHostContext(this, testName);
_debugger = new DapDebugger();
_debugger.Initialize(hc);
return hc;
}
private static async Task WithEnvironmentVariableAsync(string name, string value, Func<Task> action)
{
var originalValue = Environment.GetEnvironmentVariable(name);
Environment.SetEnvironmentVariable(name, value);
try
{
await action();
}
finally
{
Environment.SetEnvironmentVariable(name, originalValue);
}
}
private static void WithEnvironmentVariable(string name, string value, Action action)
{
var originalValue = Environment.GetEnvironmentVariable(name);
Environment.SetEnvironmentVariable(name, value);
try
{
action();
}
finally
{
Environment.SetEnvironmentVariable(name, originalValue);
}
}
private static int GetFreePort()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
return ((IPEndPoint)listener.LocalEndpoint).Port;
}
private static async Task<TcpClient> ConnectClientAsync(int port)
{
var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port);
return client;
}
private static async Task SendRequestAsync(NetworkStream stream, Request request)
{
var json = JsonConvert.SerializeObject(request);
var body = Encoding.UTF8.GetBytes(json);
var header = $"Content-Length: {body.Length}\r\n\r\n";
var headerBytes = Encoding.ASCII.GetBytes(header);
await stream.WriteAsync(headerBytes, 0, headerBytes.Length);
await stream.WriteAsync(body, 0, body.Length);
await stream.FlushAsync();
}
/// <summary>
/// Reads a single DAP-framed message from a stream with a timeout.
/// Parses the Content-Length header, reads exactly that many bytes,
/// and returns the JSON body. Fails with a clear error on timeout.
/// </summary>
private static async Task<string> ReadDapMessageAsync(NetworkStream stream, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
var token = cts.Token;
var headerBuilder = new StringBuilder();
var buffer = new byte[1];
var contentLength = -1;
while (true)
{
var readTask = stream.ReadAsync(buffer, 0, 1, token);
var bytesRead = await readTask;
if (bytesRead == 0)
{
throw new EndOfStreamException("Connection closed while reading DAP headers");
}
headerBuilder.Append((char)buffer[0]);
var headers = headerBuilder.ToString();
if (headers.EndsWith("\r\n\r\n", StringComparison.Ordinal))
{
foreach (var line in headers.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith("Content-Length: ", StringComparison.OrdinalIgnoreCase))
{
contentLength = int.Parse(line.Substring("Content-Length: ".Length).Trim());
}
}
break;
}
}
if (contentLength < 0)
{
throw new InvalidOperationException("No Content-Length header found in DAP message");
}
var body = new byte[contentLength];
var totalRead = 0;
while (totalRead < contentLength)
{
var bytesRead = await stream.ReadAsync(body, totalRead, contentLength - totalRead, token);
if (bytesRead == 0)
{
throw new EndOfStreamException("Connection closed while reading DAP body");
}
totalRead += bytesRead;
}
return Encoding.UTF8.GetString(body);
}
private static Mock<IExecutionContext> CreateJobContext(CancellationToken cancellationToken, string jobName = null)
{
var jobContext = new Mock<IExecutionContext>();
jobContext.Setup(x => x.CancellationToken).Returns(cancellationToken);
jobContext
.Setup(x => x.GetGitHubContext(It.IsAny<string>()))
.Returns((string contextName) => string.Equals(contextName, "job", StringComparison.Ordinal) ? jobName : null);
return jobContext;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void InitializeSucceeds()
{
using (CreateTestContext())
{
Assert.NotNull(_debugger);
Assert.False(_debugger.IsActive);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResolvePortUsesCustomPortFromEnvironment()
{
using (CreateTestContext())
{
WithEnvironmentVariable(PortEnvironmentVariable, "9999", () =>
{
Assert.Equal(9999, _debugger.ResolvePort());
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResolvePortIgnoresInvalidPortFromEnvironment()
{
using (CreateTestContext())
{
WithEnvironmentVariable(PortEnvironmentVariable, "not-a-number", () =>
{
Assert.Equal(4711, _debugger.ResolvePort());
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResolvePortIgnoresOutOfRangePortFromEnvironment()
{
using (CreateTestContext())
{
WithEnvironmentVariable(PortEnvironmentVariable, "99999", () =>
{
Assert.Equal(4711, _debugger.ResolvePort());
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResolveTimeoutUsesCustomTimeoutFromEnvironment()
{
using (CreateTestContext())
{
WithEnvironmentVariable(TimeoutEnvironmentVariable, "30", () =>
{
Assert.Equal(30, _debugger.ResolveTimeout());
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResolveTimeoutIgnoresInvalidTimeoutFromEnvironment()
{
using (CreateTestContext())
{
WithEnvironmentVariable(TimeoutEnvironmentVariable, "not-a-number", () =>
{
Assert.Equal(15, _debugger.ResolveTimeout());
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResolveTimeoutIgnoresZeroTimeoutFromEnvironment()
{
using (CreateTestContext())
{
WithEnvironmentVariable(TimeoutEnvironmentVariable, "0", () =>
{
Assert.Equal(15, _debugger.ResolveTimeout());
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task StartAndStopLifecycle()
{
using (CreateTestContext())
{
var port = GetFreePort();
await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () =>
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContext(cts.Token);
await _debugger.StartAsync(jobContext.Object);
using var client = await ConnectClientAsync(port);
Assert.True(client.Connected);
await _debugger.StopAsync();
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task StartAndStopMultipleTimesDoesNotThrow()
{
using (CreateTestContext())
{
foreach (var port in new[] { GetFreePort(), GetFreePort() })
{
await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () =>
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContext(cts.Token);
await _debugger.StartAsync(jobContext.Object);
await _debugger.StopAsync();
});
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WaitUntilReadyCompletesAfterClientConnectionAndConfigurationDone()
{
using (CreateTestContext())
{
var port = GetFreePort();
await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () =>
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContext(cts.Token);
await _debugger.StartAsync(jobContext.Object);
var waitTask = _debugger.WaitUntilReadyAsync();
using var client = await ConnectClientAsync(port);
await SendRequestAsync(client.GetStream(), new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
await waitTask;
Assert.Equal(DapSessionState.Ready, _debugger.State);
await _debugger.StopAsync();
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task StartStoresJobContextForThreadsRequest()
{
using (CreateTestContext())
{
var port = GetFreePort();
await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () =>
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContext(cts.Token, "ci-job");
await _debugger.StartAsync(jobContext.Object);
using var client = await ConnectClientAsync(port);
var stream = client.GetStream();
await SendRequestAsync(client.GetStream(), new Request
{
Seq = 1,
Type = "request",
Command = "threads"
});
var response = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"threads\"", response);
Assert.Contains("\"name\":\"Job: ci-job\"", response);
await _debugger.StopAsync();
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WaitUntilReadyRegistersCancellation()
{
using (CreateTestContext())
{
var port = GetFreePort();
await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () =>
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContext(cts.Token);
await _debugger.StartAsync(jobContext.Object);
var waitTask = _debugger.WaitUntilReadyAsync();
using var client = await ConnectClientAsync(port);
await SendRequestAsync(client.GetStream(), new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
await waitTask;
cts.Cancel();
Assert.True(SpinWait.SpinUntil(() => _debugger.State == DapSessionState.Terminated, TimeSpan.FromSeconds(5)));
await _debugger.StopAsync();
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task StopWithoutStartDoesNotThrow()
{
using (CreateTestContext())
{
await _debugger.StopAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task OnJobCompletedTerminatesSession()
{
using (CreateTestContext())
{
var port = GetFreePort();
await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () =>
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContext(cts.Token);
await _debugger.StartAsync(jobContext.Object);
var waitTask = _debugger.WaitUntilReadyAsync();
using var client = await ConnectClientAsync(port);
await SendRequestAsync(client.GetStream(), new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
await waitTask;
await _debugger.OnJobCompletedAsync();
Assert.Equal(DapSessionState.Terminated, _debugger.State);
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WaitUntilReadyBeforeStartIsNoOp()
{
using (CreateTestContext())
{
await _debugger.WaitUntilReadyAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WaitUntilReadyJobCancellationPropagatesAsOperationCancelledException()
{
using (CreateTestContext())
{
var port = GetFreePort();
await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () =>
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContext(cts.Token);
await _debugger.StartAsync(jobContext.Object);
var waitTask = _debugger.WaitUntilReadyAsync();
await Task.Delay(50);
cts.Cancel();
var ex = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => waitTask);
Assert.IsNotType<TimeoutException>(ex);
await _debugger.StopAsync();
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task InitializeRequestOverSocketPreservesProtocolMetadataWhenSecretsCollide()
{
using (var hc = CreateTestContext())
{
hc.SecretMasker.AddValue("response");
hc.SecretMasker.AddValue("initialize");
hc.SecretMasker.AddValue("event");
hc.SecretMasker.AddValue("initialized");
var port = GetFreePort();
await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () =>
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContext(cts.Token);
await _debugger.StartAsync(jobContext.Object);
using var client = await ConnectClientAsync(port);
var stream = client.GetStream();
await SendRequestAsync(stream, new Request
{
Seq = 1,
Type = "request",
Command = "initialize"
});
var response = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"type\":\"response\"", response);
Assert.Contains("\"command\":\"initialize\"", response);
Assert.Contains("\"success\":true", response);
var initializedEvent = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"type\":\"event\"", initializedEvent);
Assert.Contains("\"event\":\"initialized\"", initializedEvent);
await _debugger.StopAsync();
});
}
}
}
}

View File

@@ -0,0 +1,233 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
using GitHub.Runner.Worker.Dap;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class DapMessagesL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void RequestSerializesCorrectly()
{
var request = new Request
{
Seq = 1,
Type = "request",
Command = "initialize",
Arguments = JObject.FromObject(new { clientID = "test-client" })
};
var json = JsonConvert.SerializeObject(request);
var deserialized = JsonConvert.DeserializeObject<Request>(json);
Assert.Equal(1, deserialized.Seq);
Assert.Equal("request", deserialized.Type);
Assert.Equal("initialize", deserialized.Command);
Assert.Equal("test-client", deserialized.Arguments["clientID"].ToString());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResponseSerializesCorrectly()
{
var response = new Response
{
Seq = 2,
Type = "response",
RequestSeq = 1,
Success = true,
Command = "initialize",
Body = new Capabilities { SupportsConfigurationDoneRequest = true }
};
var json = JsonConvert.SerializeObject(response);
var deserialized = JsonConvert.DeserializeObject<Response>(json);
Assert.Equal(2, deserialized.Seq);
Assert.Equal("response", deserialized.Type);
Assert.Equal(1, deserialized.RequestSeq);
Assert.True(deserialized.Success);
Assert.Equal("initialize", deserialized.Command);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EventSerializesWithCorrectType()
{
var evt = new Event
{
EventType = "stopped",
Body = new StoppedEventBody
{
Reason = "entry",
Description = "Stopped at entry",
ThreadId = 1,
AllThreadsStopped = true
}
};
Assert.Equal("event", evt.Type);
var json = JsonConvert.SerializeObject(evt);
Assert.Contains("\"type\":\"event\"", json);
Assert.Contains("\"event\":\"stopped\"", json);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void StoppedEventBodyOmitsNullFields()
{
var body = new StoppedEventBody
{
Reason = "step"
};
var json = JsonConvert.SerializeObject(body);
Assert.Contains("\"reason\":\"step\"", json);
Assert.DoesNotContain("\"threadId\"", json);
Assert.DoesNotContain("\"allThreadsStopped\"", json);
Assert.DoesNotContain("\"description\"", json);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CapabilitiesMvpDefaults()
{
var caps = new Capabilities
{
SupportsConfigurationDoneRequest = true,
SupportsFunctionBreakpoints = false,
SupportsStepBack = false
};
var json = JsonConvert.SerializeObject(caps);
var deserialized = JsonConvert.DeserializeObject<Capabilities>(json);
Assert.True(deserialized.SupportsConfigurationDoneRequest);
Assert.False(deserialized.SupportsFunctionBreakpoints);
Assert.False(deserialized.SupportsStepBack);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ContinueResponseBodySerialization()
{
var body = new ContinueResponseBody { AllThreadsContinued = true };
var json = JsonConvert.SerializeObject(body);
var deserialized = JsonConvert.DeserializeObject<ContinueResponseBody>(json);
Assert.True(deserialized.AllThreadsContinued);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ThreadsResponseBodySerialization()
{
var body = new ThreadsResponseBody
{
Threads = new List<Thread>
{
new Thread { Id = 1, Name = "Job Thread" }
}
};
var json = JsonConvert.SerializeObject(body);
var deserialized = JsonConvert.DeserializeObject<ThreadsResponseBody>(json);
Assert.Single(deserialized.Threads);
Assert.Equal(1, deserialized.Threads[0].Id);
Assert.Equal("Job Thread", deserialized.Threads[0].Name);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void StackFrameSerialization()
{
var frame = new StackFrame
{
Id = 1,
Name = "Step: Checkout",
Line = 1,
Column = 1,
PresentationHint = "normal"
};
var json = JsonConvert.SerializeObject(frame);
var deserialized = JsonConvert.DeserializeObject<StackFrame>(json);
Assert.Equal(1, deserialized.Id);
Assert.Equal("Step: Checkout", deserialized.Name);
Assert.Equal("normal", deserialized.PresentationHint);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExitedEventBodySerialization()
{
var body = new ExitedEventBody { ExitCode = 130 };
var json = JsonConvert.SerializeObject(body);
var deserialized = JsonConvert.DeserializeObject<ExitedEventBody>(json);
Assert.Equal(130, deserialized.ExitCode);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void DapCommandEnumValues()
{
Assert.Equal(0, (int)DapCommand.Continue);
Assert.Equal(1, (int)DapCommand.Next);
Assert.Equal(4, (int)DapCommand.Disconnect);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void RequestDeserializesFromRawJson()
{
var json = @"{""seq"":5,""type"":""request"",""command"":""continue"",""arguments"":{""threadId"":1}}";
var request = JsonConvert.DeserializeObject<Request>(json);
Assert.Equal(5, request.Seq);
Assert.Equal("request", request.Type);
Assert.Equal("continue", request.Command);
Assert.Equal(1, request.Arguments["threadId"].Value<int>());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ErrorResponseBodySerialization()
{
var body = new ErrorResponseBody
{
Error = new Message
{
Id = 1,
Format = "Something went wrong",
ShowUser = true
}
};
var json = JsonConvert.SerializeObject(body);
var deserialized = JsonConvert.DeserializeObject<ErrorResponseBody>(json);
Assert.Equal(1, deserialized.Error.Id);
Assert.Equal("Something went wrong", deserialized.Error.Format);
Assert.True(deserialized.Error.ShowUser);
}
}
}

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.Expressions2;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Dap;
using Moq;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class DapReplExecutorL0
{
private TestHostContext _hc;
private DapReplExecutor _executor;
private List<Event> _sentEvents;
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
{
_hc = new TestHostContext(this, testName);
_sentEvents = new List<Event>();
_executor = new DapReplExecutor(_hc, (category, text) =>
{
_sentEvents.Add(new Event
{
EventType = "output",
Body = new OutputEventBody
{
Category = category,
Output = text
}
});
});
return _hc;
}
private Mock<IExecutionContext> CreateMockContext(
DictionaryContextData exprValues = null,
IDictionary<string, IDictionary<string, string>> jobDefaults = null)
{
var mock = new Mock<IExecutionContext>();
mock.Setup(x => x.ExpressionValues).Returns(exprValues ?? new DictionaryContextData());
mock.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
var global = new GlobalContext
{
PrependPath = new List<string>(),
JobDefaults = jobDefaults
?? new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase),
};
mock.Setup(x => x.Global).Returns(global);
return mock;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task ExecuteRunCommand_NullContext_ReturnsError()
{
using (CreateTestContext())
{
var command = new RunCommand { Script = "echo hello" };
var result = await _executor.ExecuteRunCommandAsync(command, null, CancellationToken.None);
Assert.Equal("error", result.Type);
Assert.Contains("No execution context available", result.Result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExpandExpressions_NoExpressions_ReturnsInput()
{
using (CreateTestContext())
{
var context = CreateMockContext();
var result = _executor.ExpandExpressions("echo hello", context.Object);
Assert.Equal("echo hello", result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExpandExpressions_NullInput_ReturnsEmpty()
{
using (CreateTestContext())
{
var context = CreateMockContext();
var result = _executor.ExpandExpressions(null, context.Object);
Assert.Equal(string.Empty, result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExpandExpressions_EmptyInput_ReturnsEmpty()
{
using (CreateTestContext())
{
var context = CreateMockContext();
var result = _executor.ExpandExpressions("", context.Object);
Assert.Equal(string.Empty, result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExpandExpressions_UnterminatedExpression_KeepsLiteral()
{
using (CreateTestContext())
{
var context = CreateMockContext();
var result = _executor.ExpandExpressions("echo ${{ github.repo", context.Object);
Assert.Equal("echo ${{ github.repo", result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResolveDefaultShell_NoJobDefaults_ReturnsPlatformDefault()
{
using (CreateTestContext())
{
var context = CreateMockContext();
var result = _executor.ResolveDefaultShell(context.Object);
#if OS_WINDOWS
Assert.True(result == "pwsh" || result == "powershell");
#else
Assert.Equal("sh", result);
#endif
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResolveDefaultShell_WithJobDefault_ReturnsJobDefault()
{
using (CreateTestContext())
{
var jobDefaults = new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase)
{
["run"] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["shell"] = "bash"
}
};
var context = CreateMockContext(jobDefaults: jobDefaults);
var result = _executor.ResolveDefaultShell(context.Object);
Assert.Equal("bash", result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void BuildEnvironment_MergesEnvContextAndReplOverrides()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
var envData = new DictionaryContextData
{
["FOO"] = new StringContextData("bar"),
};
exprValues["env"] = envData;
var context = CreateMockContext(exprValues);
var replEnv = new Dictionary<string, string> { { "BAZ", "qux" } };
var result = _executor.BuildEnvironment(context.Object, replEnv);
Assert.Equal("bar", result["FOO"]);
Assert.Equal("qux", result["BAZ"]);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void BuildEnvironment_ReplOverridesWin()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
var envData = new DictionaryContextData
{
["FOO"] = new StringContextData("original"),
};
exprValues["env"] = envData;
var context = CreateMockContext(exprValues);
var replEnv = new Dictionary<string, string> { { "FOO", "override" } };
var result = _executor.BuildEnvironment(context.Object, replEnv);
Assert.Equal("override", result["FOO"]);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void BuildEnvironment_NullReplEnv_ReturnsContextEnvOnly()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
var envData = new DictionaryContextData
{
["FOO"] = new StringContextData("bar"),
};
exprValues["env"] = envData;
var context = CreateMockContext(exprValues);
var result = _executor.BuildEnvironment(context.Object, null);
Assert.Equal("bar", result["FOO"]);
Assert.False(result.ContainsKey("BAZ"));
}
}
}
}

View File

@@ -0,0 +1,314 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Worker.Dap;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class DapReplParserL0
{
#region help command
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_HelpReturnsHelpCommand()
{
var cmd = DapReplParser.TryParse("help", out var error);
Assert.Null(error);
var help = Assert.IsType<HelpCommand>(cmd);
Assert.Null(help.Topic);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_HelpCaseInsensitive()
{
var cmd = DapReplParser.TryParse("Help", out var error);
Assert.Null(error);
Assert.IsType<HelpCommand>(cmd);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_HelpWithTopic()
{
var cmd = DapReplParser.TryParse("help(\"run\")", out var error);
Assert.Null(error);
var help = Assert.IsType<HelpCommand>(cmd);
Assert.Equal("run", help.Topic);
}
#endregion
#region run command basic
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunSimpleScript()
{
var cmd = DapReplParser.TryParse("run(\"echo hello\")", out var error);
Assert.Null(error);
var run = Assert.IsType<RunCommand>(cmd);
Assert.Equal("echo hello", run.Script);
Assert.Null(run.Shell);
Assert.Null(run.Env);
Assert.Null(run.WorkingDirectory);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunWithShell()
{
var cmd = DapReplParser.TryParse("run(\"echo hello\", shell: \"bash\")", out var error);
Assert.Null(error);
var run = Assert.IsType<RunCommand>(cmd);
Assert.Equal("echo hello", run.Script);
Assert.Equal("bash", run.Shell);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunWithWorkingDirectory()
{
var cmd = DapReplParser.TryParse("run(\"ls\", working_directory: \"/tmp\")", out var error);
Assert.Null(error);
var run = Assert.IsType<RunCommand>(cmd);
Assert.Equal("ls", run.Script);
Assert.Equal("/tmp", run.WorkingDirectory);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunWithEnv()
{
var cmd = DapReplParser.TryParse("run(\"echo $FOO\", env: { FOO: \"bar\" })", out var error);
Assert.Null(error);
var run = Assert.IsType<RunCommand>(cmd);
Assert.Equal("echo $FOO", run.Script);
Assert.NotNull(run.Env);
Assert.Equal("bar", run.Env["FOO"]);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunWithMultipleEnvVars()
{
var cmd = DapReplParser.TryParse("run(\"echo\", env: { A: \"1\", B: \"2\" })", out var error);
Assert.Null(error);
var run = Assert.IsType<RunCommand>(cmd);
Assert.Equal(2, run.Env.Count);
Assert.Equal("1", run.Env["A"]);
Assert.Equal("2", run.Env["B"]);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunWithAllOptions()
{
var input = "run(\"echo $X\", shell: \"zsh\", env: { X: \"1\" }, working_directory: \"/tmp\")";
var cmd = DapReplParser.TryParse(input, out var error);
Assert.Null(error);
var run = Assert.IsType<RunCommand>(cmd);
Assert.Equal("echo $X", run.Script);
Assert.Equal("zsh", run.Shell);
Assert.Equal("1", run.Env["X"]);
Assert.Equal("/tmp", run.WorkingDirectory);
}
#endregion
#region run command edge cases
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunWithEscapedQuotes()
{
var cmd = DapReplParser.TryParse("run(\"echo \\\"hello\\\"\")", out var error);
Assert.Null(error);
var run = Assert.IsType<RunCommand>(cmd);
Assert.Equal("echo \"hello\"", run.Script);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunWithCommaInEnvValue()
{
var cmd = DapReplParser.TryParse("run(\"echo\", env: { CSV: \"a,b,c\" })", out var error);
Assert.Null(error);
var run = Assert.IsType<RunCommand>(cmd);
Assert.Equal("a,b,c", run.Env["CSV"]);
}
#endregion
#region error cases
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunEmptyArgsReturnsError()
{
var cmd = DapReplParser.TryParse("run()", out var error);
Assert.NotNull(error);
Assert.Null(cmd);
Assert.Contains("requires a script argument", error);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunUnquotedArgReturnsError()
{
var cmd = DapReplParser.TryParse("run(echo hello)", out var error);
Assert.NotNull(error);
Assert.Null(cmd);
Assert.Contains("quoted string", error);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunUnknownOptionReturnsError()
{
var cmd = DapReplParser.TryParse("run(\"echo\", timeout: \"10\")", out var error);
Assert.NotNull(error);
Assert.Null(cmd);
Assert.Contains("Unknown option", error);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_RunMissingClosingParenReturnsError()
{
var cmd = DapReplParser.TryParse("run(\"echo\"", out var error);
Assert.NotNull(error);
Assert.Null(cmd);
}
#endregion
#region non-DSL input falls through
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_ExpressionReturnsNull()
{
var cmd = DapReplParser.TryParse("github.repository", out var error);
Assert.Null(error);
Assert.Null(cmd);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_WrappedExpressionReturnsNull()
{
var cmd = DapReplParser.TryParse("${{ github.event_name }}", out var error);
Assert.Null(error);
Assert.Null(cmd);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Parse_EmptyInputReturnsNull()
{
var cmd = DapReplParser.TryParse("", out var error);
Assert.Null(error);
Assert.Null(cmd);
cmd = DapReplParser.TryParse(null, out error);
Assert.Null(error);
Assert.Null(cmd);
}
#endregion
#region help text
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetGeneralHelp_ContainsCommands()
{
var help = DapReplParser.GetGeneralHelp();
Assert.Contains("help", help);
Assert.Contains("run", help);
Assert.Contains("expression", help, System.StringComparison.OrdinalIgnoreCase);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetRunHelp_ContainsOptions()
{
var help = DapReplParser.GetRunHelp();
Assert.Contains("shell", help);
Assert.Contains("env", help);
Assert.Contains("working_directory", help);
}
#endregion
#region internal parser helpers
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SplitArguments_HandlesNestedBraces()
{
var args = DapReplParser.SplitArguments("\"hello\", env: { A: \"1\", B: \"2\" }", out var error);
Assert.Null(error);
Assert.Equal(2, args.Count);
Assert.Equal("\"hello\"", args[0].Trim());
Assert.Contains("A:", args[1]);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ParseEnvBlock_HandlesEmptyBlock()
{
var result = DapReplParser.ParseEnvBlock("{ }", out var error);
Assert.Null(error);
Assert.NotNull(result);
Assert.Empty(result);
}
#endregion
}
}

View File

@@ -0,0 +1,772 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Dap;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class DapVariableProviderL0
{
private TestHostContext _hc;
private DapVariableProvider _provider;
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
{
_hc = new TestHostContext(this, testName);
_provider = new DapVariableProvider(_hc);
return _hc;
}
private Moq.Mock<GitHub.Runner.Worker.IExecutionContext> CreateMockContext(DictionaryContextData expressionValues)
{
var mock = new Moq.Mock<GitHub.Runner.Worker.IExecutionContext>();
mock.Setup(x => x.ExpressionValues).Returns(expressionValues);
return mock;
}
#region GetScopes tests
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetScopes_ReturnsEmptyWhenContextIsNull()
{
using (CreateTestContext())
{
var scopes = _provider.GetScopes(null);
Assert.Empty(scopes);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetScopes_ReturnsOnlyPopulatedScopes()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["github"] = new DictionaryContextData
{
{ "repository", new StringContextData("owner/repo") }
};
exprValues["env"] = new DictionaryContextData
{
{ "CI", new StringContextData("true") },
{ "HOME", new StringContextData("/home/runner") }
};
// "runner" is not set — should not appear in scopes
var ctx = CreateMockContext(exprValues);
var scopes = _provider.GetScopes(ctx.Object);
Assert.Equal(2, scopes.Count);
Assert.Equal("github", scopes[0].Name);
Assert.Equal("env", scopes[1].Name);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetScopes_ReportsNamedVariableCount()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["env"] = new DictionaryContextData
{
{ "A", new StringContextData("1") },
{ "B", new StringContextData("2") },
{ "C", new StringContextData("3") }
};
var ctx = CreateMockContext(exprValues);
var scopes = _provider.GetScopes(ctx.Object);
Assert.Single(scopes);
Assert.Equal(3, scopes[0].NamedVariables);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetScopes_SecretsGetSpecialPresentationHint()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["secrets"] = new DictionaryContextData
{
{ "MY_SECRET", new StringContextData("super-secret") }
};
exprValues["env"] = new DictionaryContextData
{
{ "CI", new StringContextData("true") }
};
var ctx = CreateMockContext(exprValues);
var scopes = _provider.GetScopes(ctx.Object);
var envScope = scopes.Find(s => s.Name == "env");
var secretsScope = scopes.Find(s => s.Name == "secrets");
Assert.NotNull(envScope);
Assert.Null(envScope.PresentationHint);
Assert.NotNull(secretsScope);
Assert.Equal("registers", secretsScope.PresentationHint);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetScopes_StableVariablesReferenceIds()
{
using (CreateTestContext())
{
// Populate all 10 scopes and verify their reference IDs
// are stable and based on array position
var exprValues = new DictionaryContextData();
foreach (var name in DapVariableProvider.ScopeNames)
{
exprValues[name] = new DictionaryContextData();
}
var ctx = CreateMockContext(exprValues);
var scopes = _provider.GetScopes(ctx.Object);
Assert.Equal(DapVariableProvider.ScopeNames.Length, scopes.Count);
for (int i = 0; i < scopes.Count; i++)
{
Assert.Equal(DapVariableProvider.ScopeNames[i], scopes[i].Name);
// Reference IDs are 1-based: index 0 -> ref 1, index 1 -> ref 2, etc.
Assert.Equal(i + 1, scopes[i].VariablesReference);
}
}
}
#endregion
#region GetVariables basic types
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_ReturnsEmptyWhenContextIsNull()
{
using (CreateTestContext())
{
var variables = _provider.GetVariables(null, 1);
Assert.Empty(variables);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_ReturnsStringVariables()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["env"] = new DictionaryContextData
{
{ "CI", new StringContextData("true") },
{ "HOME", new StringContextData("/home/runner") }
};
var ctx = CreateMockContext(exprValues);
// "env" is at ScopeNames index 1 → variablesReference = 2
var variables = _provider.GetVariables(ctx.Object, 2);
Assert.Equal(2, variables.Count);
var ciVar = variables.Find(v => v.Name == "CI");
Assert.NotNull(ciVar);
Assert.Equal("true", ciVar.Value);
Assert.Equal("string", ciVar.Type);
Assert.Equal(0, ciVar.VariablesReference);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_ReturnsBooleanVariables()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["github"] = new DictionaryContextData
{
{ "event_name", new StringContextData("push") },
};
// Use a nested dict with boolean to test
var jobDict = new DictionaryContextData();
// BooleanContextData is a valid PipelineContextData type
// but job context typically has strings. Use env scope instead.
exprValues["env"] = new DictionaryContextData
{
{ "flag", new BooleanContextData(true) }
};
var ctx = CreateMockContext(exprValues);
// "env" is at index 1 → ref 2
var variables = _provider.GetVariables(ctx.Object, 2);
var flagVar = variables.Find(v => v.Name == "flag");
Assert.NotNull(flagVar);
Assert.Equal("true", flagVar.Value);
Assert.Equal("boolean", flagVar.Type);
Assert.Equal(0, flagVar.VariablesReference);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_ReturnsNumberVariables()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["env"] = new DictionaryContextData
{
{ "count", new NumberContextData(42) }
};
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 2);
var countVar = variables.Find(v => v.Name == "count");
Assert.NotNull(countVar);
Assert.Equal("42", countVar.Value);
Assert.Equal("number", countVar.Type);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_HandlesNullValues()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
var dict = new DictionaryContextData();
dict["present"] = new StringContextData("yes");
dict["missing"] = null;
exprValues["env"] = dict;
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 2);
var nullVar = variables.Find(v => v.Name == "missing");
Assert.NotNull(nullVar);
Assert.Equal("null", nullVar.Value);
Assert.Equal("null", nullVar.Type);
Assert.Equal(0, nullVar.VariablesReference);
}
}
#endregion
#region GetVariables nested expansion
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_NestedDictionaryIsExpandable()
{
using (CreateTestContext())
{
var innerDict = new DictionaryContextData
{
{ "name", new StringContextData("push") },
{ "ref", new StringContextData("refs/heads/main") }
};
var exprValues = new DictionaryContextData();
exprValues["github"] = new DictionaryContextData
{
{ "event", innerDict }
};
var ctx = CreateMockContext(exprValues);
// "github" is at index 0 → ref 1
var variables = _provider.GetVariables(ctx.Object, 1);
var eventVar = variables.Find(v => v.Name == "event");
Assert.NotNull(eventVar);
Assert.Equal("object", eventVar.Type);
Assert.True(eventVar.VariablesReference > 0, "Nested dict should have a non-zero variablesReference");
Assert.Equal(2, eventVar.NamedVariables);
// Now expand it
var children = _provider.GetVariables(ctx.Object, eventVar.VariablesReference);
Assert.Equal(2, children.Count);
var nameVar = children.Find(v => v.Name == "name");
Assert.NotNull(nameVar);
Assert.Equal("push", nameVar.Value);
Assert.Equal("${{ github.event.name }}", nameVar.EvaluateName);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_NestedArrayIsExpandable()
{
using (CreateTestContext())
{
var array = new ArrayContextData();
array.Add(new StringContextData("item0"));
array.Add(new StringContextData("item1"));
var exprValues = new DictionaryContextData();
exprValues["env"] = new DictionaryContextData
{
{ "list", array }
};
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 2);
var listVar = variables.Find(v => v.Name == "list");
Assert.NotNull(listVar);
Assert.Equal("array", listVar.Type);
Assert.True(listVar.VariablesReference > 0);
Assert.Equal(2, listVar.IndexedVariables);
// Expand the array
var items = _provider.GetVariables(ctx.Object, listVar.VariablesReference);
Assert.Equal(2, items.Count);
Assert.Equal("[0]", items[0].Name);
Assert.Equal("item0", items[0].Value);
Assert.Equal("[1]", items[1].Name);
Assert.Equal("item1", items[1].Value);
}
}
#endregion
#region Secret masking
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_SecretsScopeValuesAreRedacted()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["secrets"] = new DictionaryContextData
{
{ "MY_TOKEN", new StringContextData("ghp_abc123secret") },
{ "DB_PASSWORD", new StringContextData("p@ssword!") }
};
var ctx = CreateMockContext(exprValues);
// "secrets" is at index 5 → ref 6
var variables = _provider.GetVariables(ctx.Object, 6);
Assert.Equal(2, variables.Count);
foreach (var v in variables)
{
Assert.Equal(DapVariableProvider.RedactedValue, v.Value);
Assert.Equal("string", v.Type);
}
// Keys should still be visible
Assert.Contains(variables, v => v.Name == "MY_TOKEN");
Assert.Contains(variables, v => v.Name == "DB_PASSWORD");
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_NonSecretScopeValuesMaskedBySecretMasker()
{
using (var hc = CreateTestContext())
{
// Register a known secret value with the masker
hc.SecretMasker.AddValue("super-secret-token");
var exprValues = new DictionaryContextData();
exprValues["env"] = new DictionaryContextData
{
{ "SAFE", new StringContextData("hello world") },
{ "LEAKED", new StringContextData("prefix-super-secret-token-suffix") }
};
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 2);
var safeVar = variables.Find(v => v.Name == "SAFE");
Assert.NotNull(safeVar);
Assert.Equal("hello world", safeVar.Value);
var leakedVar = variables.Find(v => v.Name == "LEAKED");
Assert.NotNull(leakedVar);
Assert.DoesNotContain("super-secret-token", leakedVar.Value);
Assert.Contains("***", leakedVar.Value);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void MaskSecrets_DelegatesToHostContextSecretMasker()
{
using (var hc = CreateTestContext())
{
hc.SecretMasker.AddValue("my-secret");
Assert.Equal("before-***-after", _provider.MaskSecrets("before-my-secret-after"));
Assert.Equal("no secrets here", _provider.MaskSecrets("no secrets here"));
Assert.Equal(string.Empty, _provider.MaskSecrets(null));
Assert.Equal(string.Empty, _provider.MaskSecrets(string.Empty));
}
}
#endregion
#region Reset
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Reset_InvalidatesNestedReferences()
{
using (CreateTestContext())
{
var innerDict = new DictionaryContextData
{
{ "name", new StringContextData("push") }
};
var exprValues = new DictionaryContextData();
exprValues["github"] = new DictionaryContextData
{
{ "event", innerDict }
};
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 1);
var eventVar = variables.Find(v => v.Name == "event");
Assert.True(eventVar.VariablesReference > 0);
var savedRef = eventVar.VariablesReference;
// Reset should clear all dynamic references
_provider.Reset();
var children = _provider.GetVariables(ctx.Object, savedRef);
Assert.Empty(children);
}
}
#endregion
#region EvaluateName
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_SetsEvaluateNameWithDotPath()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["github"] = new DictionaryContextData
{
{ "repository", new StringContextData("owner/repo") }
};
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 1);
var repoVar = variables.Find(v => v.Name == "repository");
Assert.NotNull(repoVar);
Assert.Equal("${{ github.repository }}", repoVar.EvaluateName);
}
}
#endregion
#region EvaluateExpression
/// <summary>
/// Creates a mock execution context with Global set up so that
/// ToPipelineTemplateEvaluator() works for real expression evaluation.
/// </summary>
private Moq.Mock<IExecutionContext> CreateEvaluatableContext(
TestHostContext hc,
DictionaryContextData expressionValues)
{
var mock = new Moq.Mock<IExecutionContext>();
mock.Setup(x => x.ExpressionValues).Returns(expressionValues);
mock.Setup(x => x.ExpressionFunctions)
.Returns(new List<GitHub.DistributedTask.Expressions2.IFunctionInfo>());
mock.Setup(x => x.Global).Returns(new GlobalContext
{
FileTable = new List<string>(),
Variables = new Variables(hc, new Dictionary<string, VariableValue>()),
});
// ToPipelineTemplateEvaluator uses ToTemplateTraceWriter which calls
// context.Write — provide a no-op so it doesn't NRE.
mock.Setup(x => x.Write(Moq.It.IsAny<string>(), Moq.It.IsAny<string>()));
return mock;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateExpression_ReturnsValueForSimpleExpression()
{
using (var hc = CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["github"] = new DictionaryContextData
{
{ "repository", new StringContextData("owner/repo") }
};
var ctx = CreateEvaluatableContext(hc, exprValues);
var result = _provider.EvaluateExpression("github.repository", ctx.Object);
Assert.Equal("owner/repo", result.Result);
Assert.Equal("string", result.Type);
Assert.Equal(0, result.VariablesReference);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateExpression_StripsWrapperSyntax()
{
using (var hc = CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["github"] = new DictionaryContextData
{
{ "event_name", new StringContextData("push") }
};
var ctx = CreateEvaluatableContext(hc, exprValues);
var result = _provider.EvaluateExpression("${{ github.event_name }}", ctx.Object);
Assert.Equal("push", result.Result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateExpression_MasksSecretInResult()
{
using (var hc = CreateTestContext())
{
hc.SecretMasker.AddValue("super-secret");
var exprValues = new DictionaryContextData();
exprValues["env"] = new DictionaryContextData
{
{ "TOKEN", new StringContextData("super-secret") }
};
var ctx = CreateEvaluatableContext(hc, exprValues);
var result = _provider.EvaluateExpression("env.TOKEN", ctx.Object);
Assert.DoesNotContain("super-secret", result.Result);
Assert.Contains("***", result.Result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateExpression_ReturnsErrorForInvalidExpression()
{
using (var hc = CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["github"] = new DictionaryContextData();
var ctx = CreateEvaluatableContext(hc, exprValues);
// An invalid expression syntax should not throw — it should
// return an error result.
var result = _provider.EvaluateExpression("!!!invalid[[", ctx.Object);
Assert.Contains("error", result.Result, StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateExpression_ReturnsMessageWhenNoContext()
{
using (CreateTestContext())
{
var result = _provider.EvaluateExpression("github.repository", null);
Assert.Contains("no execution context", result.Result, StringComparison.OrdinalIgnoreCase);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateExpression_ReturnsEmptyForEmptyExpression()
{
using (var hc = CreateTestContext())
{
var exprValues = new DictionaryContextData();
var ctx = CreateEvaluatableContext(hc, exprValues);
var result = _provider.EvaluateExpression("", ctx.Object);
Assert.Equal(string.Empty, result.Result);
}
}
#endregion
#region InferResultType
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void InferResultType_ClassifiesCorrectly()
{
using (CreateTestContext())
{
Assert.Equal("null", DapVariableProvider.InferResultType(null));
Assert.Equal("null", DapVariableProvider.InferResultType("null"));
Assert.Equal("boolean", DapVariableProvider.InferResultType("true"));
Assert.Equal("boolean", DapVariableProvider.InferResultType("false"));
Assert.Equal("number", DapVariableProvider.InferResultType("42"));
Assert.Equal("number", DapVariableProvider.InferResultType("3.14"));
Assert.Equal("object", DapVariableProvider.InferResultType("{\"key\":\"val\"}"));
Assert.Equal("object", DapVariableProvider.InferResultType("[1,2,3]"));
Assert.Equal("string", DapVariableProvider.InferResultType("hello world"));
Assert.Equal("string", DapVariableProvider.InferResultType("owner/repo"));
}
}
#endregion
#region Non-string secret type redaction
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_SecretsScopeRedactsNumberContextData()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["secrets"] = new DictionaryContextData
{
{ "NUMERIC_SECRET", new NumberContextData(12345) }
};
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 6);
Assert.Single(variables);
Assert.Equal("NUMERIC_SECRET", variables[0].Name);
Assert.Equal(DapVariableProvider.RedactedValue, variables[0].Value);
Assert.Equal("string", variables[0].Type);
Assert.Equal(0, variables[0].VariablesReference);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_SecretsScopeRedactsBooleanContextData()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["secrets"] = new DictionaryContextData
{
{ "BOOL_SECRET", new BooleanContextData(true) }
};
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 6);
Assert.Single(variables);
Assert.Equal("BOOL_SECRET", variables[0].Name);
Assert.Equal(DapVariableProvider.RedactedValue, variables[0].Value);
Assert.Equal("string", variables[0].Type);
Assert.Equal(0, variables[0].VariablesReference);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_SecretsScopeRedactsNestedDictionary()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
exprValues["secrets"] = new DictionaryContextData
{
{ "NESTED_SECRET", new DictionaryContextData
{
{ "inner_key", new StringContextData("inner_value") }
}
}
};
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 6);
Assert.Single(variables);
Assert.Equal("NESTED_SECRET", variables[0].Name);
Assert.Equal(DapVariableProvider.RedactedValue, variables[0].Value);
Assert.Equal("string", variables[0].Type);
// Nested container should NOT be drillable under secrets
Assert.Equal(0, variables[0].VariablesReference);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void GetVariables_SecretsScopeRedactsNullValue()
{
using (CreateTestContext())
{
var exprValues = new DictionaryContextData();
var secrets = new DictionaryContextData();
secrets["NULL_SECRET"] = null;
exprValues["secrets"] = secrets;
var ctx = CreateMockContext(exprValues);
var variables = _provider.GetVariables(ctx.Object, 6);
Assert.Single(variables);
Assert.Equal("NULL_SECRET", variables[0].Name);
Assert.Equal(DapVariableProvider.RedactedValue, variables[0].Value);
Assert.Equal(0, variables[0].VariablesReference);
}
}
#endregion
}
}

View File

@@ -370,504 +370,5 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Contains("./.github/actions/my-action", deprecatedActions);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node20Action_TrackedAsUpgradedWhenUseNode24ByDefaultEnabled()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") },
{ Constants.Runner.NodeMigration.UseNode24ByDefaultFlag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var upgradedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
DeprecatedNode20Actions = deprecatedActions,
UpgradedToNode24Actions = upgradedActions
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v4"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node20";
var handler = hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
// On non-ARM32 platforms, action should be upgraded to node24
// and tracked in UpgradedToNode24Actions, NOT in DeprecatedNode20Actions
bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm &&
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
if (!isArm32Linux)
{
Assert.Equal("node24", handler.Data.NodeVersion);
Assert.Contains("actions/checkout@v4", upgradedActions);
Assert.DoesNotContain("actions/checkout@v4", deprecatedActions);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node20Action_NotUpgradedWhenPhase1Only()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var upgradedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
DeprecatedNode20Actions = deprecatedActions,
UpgradedToNode24Actions = upgradedActions
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v4"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node20";
var handler = hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
// In Phase 1 (no UseNode24ByDefault), action stays on node20
// and should be in DeprecatedNode20Actions
Assert.Equal("node20", handler.Data.NodeVersion);
Assert.Contains("actions/checkout@v4", deprecatedActions);
Assert.Empty(upgradedActions);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExplicitNode24Action_KillArm32Flag_ThrowsOnArm32()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.KillLinuxArm32Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>()
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v5"
};
// Act - action explicitly declares node24
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node24";
bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm &&
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
if (isArm32Linux)
{
// On ARM32 Linux, kill flag should cause the handler to throw
Assert.Throws<InvalidOperationException>(() => hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
));
}
else
{
// On other platforms, should proceed normally
var handler = hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
Assert.Equal("node24", handler.Data.NodeVersion);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExplicitNode24Action_DeprecateArm32Flag_DowngradesToNode20OnArm32()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var arm32Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
Arm32Node20Actions = arm32Actions
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v5"
};
// Act - action explicitly declares node24
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node24";
var handler = hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm &&
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
if (isArm32Linux)
{
// On ARM32 Linux, should downgrade to node20 and track
Assert.Equal("node20", handler.Data.NodeVersion);
Assert.Contains("actions/checkout@v5", arm32Actions);
}
else
{
// On other platforms, should remain node24
Assert.Equal("node24", handler.Data.NodeVersion);
Assert.Empty(arm32Actions);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExplicitNode24Action_NoArm32Flags_StaysNode24()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>();
Variables serverVariables = new(hc, variables);
var arm32Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
Arm32Node20Actions = arm32Actions,
DeprecatedNode20Actions = deprecatedActions
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v5"
};
// Act - action explicitly declares node24, no ARM32 flags
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node24";
var handler = hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
// On non-ARM32 platforms, should stay node24 and not be tracked in any list
bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm &&
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
if (!isArm32Linux)
{
Assert.Equal("node24", handler.Data.NodeVersion);
Assert.Empty(arm32Actions);
Assert.Empty(deprecatedActions);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node20Action_RequireNode24_ForcesNode24()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.RequireNode24Flag, new VariableValue("true") },
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var upgradedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
UpgradedToNode24Actions = upgradedActions,
DeprecatedNode20Actions = deprecatedActions
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v4"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node20";
bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm &&
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
if (!isArm32Linux)
{
var handler = hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
// Phase 3: RequireNode24 forces node24, ignoring env vars
Assert.Equal("node24", handler.Data.NodeVersion);
Assert.Contains("actions/checkout@v4", upgradedActions);
Assert.Empty(deprecatedActions);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node20Action_KillArm32Flag_ThrowsOnArm32()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.KillLinuxArm32Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>()
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v4"
};
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node20";
bool isArm32Linux = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm &&
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
if (isArm32Linux)
{
Assert.Throws<InvalidOperationException>(() => hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
));
}
else
{
// On non-ARM32, should proceed normally (node20 stays)
var handler = hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
) as INodeScriptActionHandler;
Assert.Equal("node20", handler.Data.NodeVersion);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExplicitNode24Action_DeprecateArm32_UsesOriginalVersionForTracking()
{
// Regression test: verifies that when an action explicitly declares node24
// and ARM32 deprecation downgrades it to node20, the tracking call uses
// the original preferred version ("node24"), not the already-overwritten
// nodeData.NodeVersion ("node20"). Without this fix, ShouldTrackAsArm32Node20
// would receive (preferred="node20", final="node20") and never return true.
string originalPreferred = "node24";
string finalAfterArm32Downgrade = "node20";
string deprecationWarning = "Linux ARM32 runners are deprecated and will no longer be supported after September 16th, 2026. Please migrate to a supported platform.";
// Correct: use the original preferred version before assignment
bool correctTracking = HandlerFactory.ShouldTrackAsArm32Node20(
deprecateArm32: true,
preferredNodeVersion: originalPreferred,
finalNodeVersion: finalAfterArm32Downgrade,
platformWarningMessage: deprecationWarning);
Assert.True(correctTracking);
// Bug scenario: if nodeData.NodeVersion was already overwritten to finalNodeVersion
bool buggyTracking = HandlerFactory.ShouldTrackAsArm32Node20(
deprecateArm32: true,
preferredNodeVersion: finalAfterArm32Downgrade,
finalNodeVersion: finalAfterArm32Downgrade,
platformWarningMessage: deprecationWarning);
Assert.False(buggyTracking);
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData(true, "node24", "node20", "Linux ARM32 runners are deprecated", true)]
[InlineData(true, "node20", "node20", "Linux ARM32 runners are deprecated", false)]
[InlineData(true, "node24", "node24", "Linux ARM32 runners are deprecated", false)]
[InlineData(true, "node24", "node20", null, false)]
[InlineData(false, "node24", "node20", "Linux ARM32 runners are deprecated", false)]
public void ShouldTrackAsArm32Node20_ClassifiesOnlyPlatformDowngrades(
bool deprecateArm32,
string preferredNodeVersion,
string finalNodeVersion,
string platformWarningMessage,
bool expected)
{
bool actual = HandlerFactory.ShouldTrackAsArm32Node20(
deprecateArm32,
preferredNodeVersion,
finalNodeVersion,
platformWarningMessage);
Assert.Equal(expected, actual);
}
}
}

View File

@@ -8,6 +8,7 @@ using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Dap;
using Moq;
using Xunit;
using Pipelines = GitHub.DistributedTask.Pipelines;
@@ -547,6 +548,10 @@ namespace GitHub.Runner.Common.Tests.Worker
var _stepsRunner = new StepsRunner();
_stepsRunner.Initialize(hc);
var mockDapDebugger = new Mock<IDapDebugger>();
hc.SetSingleton(mockDapDebugger.Object);
await _stepsRunner.RunAsync(_jobEc);
Assert.Equal("Create custom image", snapshotStep.DisplayName);

View File

@@ -19,7 +19,6 @@ namespace GitHub.Runner.Common.Tests.Worker
public sealed class PipelineTemplateEvaluatorWrapperL0
{
private CancellationTokenSource _ecTokenSource;
private CancellationTokenSource _rootTokenSource;
private Mock<IExecutionContext> _ec;
private TestHostContext _hc;
@@ -66,7 +65,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
// Call EvaluateAndCompare directly: the new evaluator cancels the root token
// Call EvaluateAndCompare directly: the new evaluator cancels the token
// and returns a different value, forcing hasMismatch = true.
// Because cancellation flipped during the evaluation window, the
// mismatch should be skipped.
@@ -75,7 +74,7 @@ namespace GitHub.Runner.Common.Tests.Worker
() => "legacy-value",
() =>
{
_rootTokenSource.Cancel();
_ecTokenSource.Cancel();
return "different-value";
},
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
@@ -89,43 +88,6 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateAndCompare_SkipsMismatchRecording_WhenRootCancellationOccursBetweenEvaluators()
{
// Simulates job-level cancellation firing between legacy and new evaluator runs.
// Root is mocked with a separate CancellationTokenSource to exercise the
// _context.Root?.CancellationToken path (the job-level token).
try
{
Setup();
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
// Legacy evaluator cancels the root token (simulating job cancel) and returns a value.
// The new evaluator returns a different value. The mismatch should be skipped.
var result = wrapper.EvaluateAndCompare<string, string>(
"TestRootCancellationSkip",
() =>
{
var legacyValue = "legacy-value";
_rootTokenSource.Cancel();
return legacyValue;
},
() => "different-value",
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
Assert.Equal("legacy-value", result);
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -900,8 +862,6 @@ namespace GitHub.Runner.Common.Tests.Worker
{
_ecTokenSource?.Dispose();
_ecTokenSource = new CancellationTokenSource();
_rootTokenSource?.Dispose();
_rootTokenSource = new CancellationTokenSource();
_hc = new TestHostContext(this, name);
@@ -917,9 +877,6 @@ namespace GitHub.Runner.Common.Tests.Worker
WriteDebug = true,
});
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
var rootEc = new Mock<IExecutionContext>();
rootEc.Setup(x => x.CancellationToken).Returns(_rootTokenSource.Token);
_ec.Setup(x => x.Root).Returns(rootEc.Object);
_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}"); });
@@ -928,8 +885,6 @@ namespace GitHub.Runner.Common.Tests.Worker
private void Teardown()
{
_ecTokenSource?.Dispose();
_rootTokenSource?.Dispose();
_hc?.Dispose();
}
}

View File

@@ -59,161 +59,5 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("node20", nodeVersion);
Assert.Null(warningMessage);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_DeprecationFlagShowsWarning()
{
string preferredVersion = "node24";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32: true);
bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true;
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isArm32 && isLinux)
{
Assert.Equal("node20", nodeVersion);
Assert.NotNull(warningMessage);
Assert.Contains("deprecated", warningMessage);
Assert.Contains("no longer be supported", warningMessage);
}
else
{
Assert.Equal("node24", nodeVersion);
Assert.Null(warningMessage);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_DeprecationFlagWithNode20PassesThrough()
{
// Even with deprecation flag, node20 should pass through (not downgraded further)
string preferredVersion = "node20";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32: true);
bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true;
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isArm32 && isLinux)
{
Assert.Equal("node20", nodeVersion);
Assert.NotNull(warningMessage);
Assert.Contains("deprecated", warningMessage);
}
else
{
Assert.Equal("node20", nodeVersion);
Assert.Null(warningMessage);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_KillFlagReturnsNull()
{
string preferredVersion = "node24";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, killArm32: true);
bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true;
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isArm32 && isLinux)
{
Assert.Null(nodeVersion);
Assert.NotNull(warningMessage);
Assert.Contains("no longer supported", warningMessage);
}
else
{
Assert.Equal("node24", nodeVersion);
Assert.Null(warningMessage);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_KillTakesPrecedenceOverDeprecation()
{
string preferredVersion = "node20";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion, deprecateArm32: true, killArm32: true);
bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true;
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isArm32 && isLinux)
{
Assert.Null(nodeVersion);
Assert.NotNull(warningMessage);
Assert.Contains("no longer supported", warningMessage);
}
else
{
Assert.Equal("node20", nodeVersion);
Assert.Null(warningMessage);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_ServerOverridableDateUsedInDeprecationWarning()
{
string preferredVersion = "node24";
string customDate = "December 1st, 2027";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(
preferredVersion, deprecateArm32: true, node20RemovalDate: customDate);
bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true;
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isArm32 && isLinux)
{
Assert.Equal("node20", nodeVersion);
Assert.NotNull(warningMessage);
Assert.Contains(customDate, warningMessage);
Assert.DoesNotContain(Constants.Runner.NodeMigration.Node20RemovalDate, warningMessage);
}
else
{
Assert.Equal("node24", nodeVersion);
Assert.Null(warningMessage);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CheckNodeVersionForArm32_FallbackDateUsedWhenNoOverride()
{
string preferredVersion = "node24";
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(
preferredVersion, deprecateArm32: true);
bool isArm32 = RuntimeInformation.ProcessArchitecture == Architecture.Arm ||
Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")?.Contains("ARM") == true;
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isArm32 && isLinux)
{
Assert.Equal("node20", nodeVersion);
Assert.NotNull(warningMessage);
Assert.Contains(Constants.Runner.NodeMigration.Node20RemovalDate, warningMessage);
}
else
{
Assert.Equal("node24", nodeVersion);
Assert.Null(warningMessage);
}
}
}
}

View File

@@ -12,6 +12,7 @@ using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Dap;
namespace GitHub.Runner.Common.Tests.Worker
{
@@ -61,6 +62,10 @@ namespace GitHub.Runner.Common.Tests.Worker
_stepsRunner = new StepsRunner();
_stepsRunner.Initialize(hc);
var mockDapDebugger = new Mock<IDapDebugger>();
hc.SetSingleton(mockDapDebugger.Object);
return hc;
}

View File

@@ -1,13 +0,0 @@
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

@@ -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.419"
DOTNETSDK_VERSION="8.0.418"
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
RUNNER_VERSION=$(cat runnerversion)

View File

@@ -1,5 +1,5 @@
{
"sdk": {
"version": "8.0.419"
"version": "8.0.418"
}
}

View File

@@ -1 +1 @@
2.333.0
2.332.0