mirror of
https://github.com/actions/runner.git
synced 2026-07-06 20:55:42 +08:00
Compare commits
1 Commits
dependabot
...
v2.330.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2088f1fc7 |
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Actions Runner Devcontainer",
|
"name": "Actions Runner Devcontainer",
|
||||||
"image": "mcr.microsoft.com/devcontainers/base:noble",
|
"image": "mcr.microsoft.com/devcontainers/base:focal",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
|
||||||
"ghcr.io/devcontainers/features/dotnet": {
|
"ghcr.io/devcontainers/features/dotnet": {
|
||||||
"version": "8.0.421"
|
"version": "8.0.416"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20"
|
"version": "20"
|
||||||
|
|||||||
52
.github/workflows/build.yml
vendored
52
.github/workflows/build.yml
vendored
@@ -14,9 +14,6 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -53,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
@@ -78,53 +75,8 @@ jobs:
|
|||||||
# Upload runner package tar.gz/zip as artifact
|
# Upload runner package tar.gz/zip as artifact
|
||||||
- name: Publish Artifact
|
- name: Publish Artifact
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-package-${{ matrix.runtime }}
|
name: runner-package-${{ matrix.runtime }}
|
||||||
path: |
|
path: |
|
||||||
_package
|
_package
|
||||||
|
|
||||||
docker:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ ubuntu-latest, ubuntu-24.04-arm ]
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
docker_platform: linux/amd64
|
|
||||||
- os: ubuntu-24.04-arm
|
|
||||||
docker_platform: linux/arm64
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Get latest runner version
|
|
||||||
id: latest_runner
|
|
||||||
uses: actions/github-script@v9
|
|
||||||
with:
|
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
script: |
|
|
||||||
const release = await github.rest.repos.getLatestRelease({
|
|
||||||
owner: 'actions',
|
|
||||||
repo: 'runner',
|
|
||||||
});
|
|
||||||
const version = release.data.tag_name.replace(/^v/, '');
|
|
||||||
core.setOutput('version', version);
|
|
||||||
|
|
||||||
- name: Setup Docker buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: ./images
|
|
||||||
load: true
|
|
||||||
platforms: ${{ matrix.docker_platform }}
|
|
||||||
tags: |
|
|
||||||
${{ github.sha }}:latest
|
|
||||||
build-args: |
|
|
||||||
RUNNER_VERSION=${{ steps.latest_runner.outputs.version }}
|
|
||||||
|
|
||||||
- name: Test Docker image
|
|
||||||
run: |
|
|
||||||
docker run --rm ${{ github.sha }}:latest ./run.sh --version
|
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
2
.github/workflows/dependency-check.yml
vendored
2
.github/workflows/dependency-check.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
npm-vulnerabilities: ${{ steps.check-versions.outputs.npm-vulnerabilities }}
|
npm-vulnerabilities: ${{ steps.check-versions.outputs.npm-vulnerabilities }}
|
||||||
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
|
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/docker-buildx-upgrade.yml
vendored
4
.github/workflows/docker-buildx-upgrade.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
BUILDX_CURRENT_VERSION: ${{ steps.check_buildx_version.outputs.CURRENT_VERSION }}
|
BUILDX_CURRENT_VERSION: ${{ steps.check_buildx_version.outputs.CURRENT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Check Docker version
|
- name: Check Docker version
|
||||||
id: check_docker_version
|
id: check_docker_version
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Update Docker version
|
- name: Update Docker version
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
75
.github/workflows/docker-publish.yml
vendored
75
.github/workflows/docker-publish.yml
vendored
@@ -1,75 +0,0 @@
|
|||||||
name: Publish DockerImage from Release Branch
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
releaseBranch:
|
|
||||||
description: 'Release Branch (releases/mXXX)'
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.inputs.releaseBranch }}
|
|
||||||
|
|
||||||
- name: Compute image version
|
|
||||||
id: image
|
|
||||||
uses: actions/github-script@v9
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
const runnerVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '');
|
|
||||||
console.log(`Using runner version ${runnerVersion}`);
|
|
||||||
if (!/^\d+\.\d+\.\d+$/.test(runnerVersion)) {
|
|
||||||
throw new Error(`Invalid runner version: ${runnerVersion}`);
|
|
||||||
}
|
|
||||||
core.setOutput('version', runnerVersion);
|
|
||||||
|
|
||||||
- name: Setup Docker buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
id: build-and-push
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: ./images
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/arm64
|
|
||||||
tags: |
|
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image.outputs.version }}
|
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
|
||||||
build-args: |
|
|
||||||
RUNNER_VERSION=${{ steps.image.outputs.version }}
|
|
||||||
push: true
|
|
||||||
labels: |
|
|
||||||
org.opencontainers.image.source=${{github.server_url}}/${{github.repository}}
|
|
||||||
org.opencontainers.image.licenses=MIT
|
|
||||||
annotations: |
|
|
||||||
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
|
|
||||||
|
|
||||||
- name: Generate attestation
|
|
||||||
uses: actions/attest-build-provenance@v4
|
|
||||||
with:
|
|
||||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
4
.github/workflows/dotnet-upgrade.yml
vendored
4
.github/workflows/dotnet-upgrade.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
|
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
- name: Get current major minor version
|
- name: Get current major minor version
|
||||||
id: fetch_current_version
|
id: fetch_current_version
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
|
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
|
|||||||
26
.github/workflows/node-upgrade.yml
vendored
26
.github/workflows/node-upgrade.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
update-node:
|
update-node:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- name: Get latest Node versions
|
- name: Get latest Node versions
|
||||||
id: node-versions
|
id: node-versions
|
||||||
run: |
|
run: |
|
||||||
@@ -159,36 +159,18 @@ jobs:
|
|||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>"
|
git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>"
|
||||||
|
|
||||||
# Build version summary for commit message and PR body (only include changed versions)
|
|
||||||
COMMIT_VERSIONS=""
|
|
||||||
PR_VERSION_LINES=""
|
|
||||||
|
|
||||||
if [ "${{ steps.node-versions.outputs.needs_update20 }}" == "true" ]; then
|
|
||||||
COMMIT_VERSIONS="20: $NODE20_VERSION"
|
|
||||||
PR_VERSION_LINES="- Node 20: ${{ steps.node-versions.outputs.current_node20 }} → $NODE20_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${{ steps.node-versions.outputs.needs_update24 }}" == "true" ]; then
|
|
||||||
if [ -n "$COMMIT_VERSIONS" ]; then
|
|
||||||
COMMIT_VERSIONS="$COMMIT_VERSIONS, 24: $NODE24_VERSION"
|
|
||||||
else
|
|
||||||
COMMIT_VERSIONS="24: $NODE24_VERSION"
|
|
||||||
fi
|
|
||||||
PR_VERSION_LINES="${PR_VERSION_LINES:+$PR_VERSION_LINES
|
|
||||||
}- Node 24: ${{ steps.node-versions.outputs.current_node24 }} → $NODE24_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create branch and commit changes
|
# Create branch and commit changes
|
||||||
branch_name="chore/update-node"
|
branch_name="chore/update-node"
|
||||||
git checkout -b "$branch_name"
|
git checkout -b "$branch_name"
|
||||||
git commit -a -m "chore: update Node versions ($COMMIT_VERSIONS)"
|
git commit -a -m "chore: update Node versions (20: $NODE20_VERSION, 24: $NODE24_VERSION)"
|
||||||
git push --force origin "$branch_name"
|
git push --force origin "$branch_name"
|
||||||
|
|
||||||
# Create PR body using here-doc for proper formatting
|
# Create PR body using here-doc for proper formatting
|
||||||
cat > pr_body.txt << EOF
|
cat > pr_body.txt << EOF
|
||||||
Automated Node.js version update:
|
Automated Node.js version update:
|
||||||
|
|
||||||
$PR_VERSION_LINES
|
- Node 20: ${{ steps.node-versions.outputs.current_node20 }} → $NODE20_VERSION
|
||||||
|
- Node 24: ${{ steps.node-versions.outputs.current_node24 }} → $NODE24_VERSION
|
||||||
|
|
||||||
This update ensures we're using the latest stable Node.js versions for security and performance improvements.
|
This update ensures we're using the latest stable Node.js versions for security and performance improvements.
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/npm-audit-typescript.yml
vendored
2
.github/workflows/npm-audit-typescript.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
npm-audit-with-ts-fix:
|
npm-audit-with-ts-fix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/npm-audit.yml
vendored
2
.github/workflows/npm-audit.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
npm-audit:
|
npm-audit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
|
|||||||
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@@ -11,12 +11,12 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
|
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# Make sure ./releaseVersion match ./src/runnerversion
|
# Make sure ./releaseVersion match ./src/runnerversion
|
||||||
# Query GitHub release ensure version is not used
|
# Query GitHub release ensure version is not used
|
||||||
- name: Check version
|
- name: Check version
|
||||||
uses: actions/github-script@v9
|
uses: actions/github-script@v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
@@ -118,7 +118,7 @@ jobs:
|
|||||||
# Upload runner package tar.gz/zip as artifact.
|
# Upload runner package tar.gz/zip as artifact.
|
||||||
- name: Publish Artifact
|
- name: Publish Artifact
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: runner-packages-${{ matrix.runtime }}
|
name: runner-packages-${{ matrix.runtime }}
|
||||||
path: |
|
path: |
|
||||||
@@ -129,41 +129,41 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
# Download runner package tar.gz/zip produced by 'build' job
|
# Download runner package tar.gz/zip produced by 'build' job
|
||||||
- name: Download Artifact (win-x64)
|
- name: Download Artifact (win-x64)
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: runner-packages-win-x64
|
name: runner-packages-win-x64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (win-arm64)
|
- name: Download Artifact (win-arm64)
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: runner-packages-win-arm64
|
name: runner-packages-win-arm64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (osx-x64)
|
- name: Download Artifact (osx-x64)
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: runner-packages-osx-x64
|
name: runner-packages-osx-x64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (osx-arm64)
|
- name: Download Artifact (osx-arm64)
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: runner-packages-osx-arm64
|
name: runner-packages-osx-arm64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (linux-x64)
|
- name: Download Artifact (linux-x64)
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: runner-packages-linux-x64
|
name: runner-packages-linux-x64
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (linux-arm)
|
- name: Download Artifact (linux-arm)
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: runner-packages-linux-arm
|
name: runner-packages-linux-arm
|
||||||
path: ./
|
path: ./
|
||||||
- name: Download Artifact (linux-arm64)
|
- name: Download Artifact (linux-arm64)
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: runner-packages-linux-arm64
|
name: runner-packages-linux-arm64
|
||||||
path: ./
|
path: ./
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
# Create ReleaseNote file
|
# Create ReleaseNote file
|
||||||
- name: Create ReleaseNote
|
- name: Create ReleaseNote
|
||||||
id: releaseNote
|
id: releaseNote
|
||||||
uses: actions/github-script@v9
|
uses: actions/github-script@v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
@@ -296,11 +296,11 @@ jobs:
|
|||||||
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Compute image version
|
- name: Compute image version
|
||||||
id: image
|
id: image
|
||||||
uses: actions/github-script@v9
|
uses: actions/github-script@v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -309,10 +309,10 @@ jobs:
|
|||||||
core.setOutput('version', runnerVersion);
|
core.setOutput('version', runnerVersion);
|
||||||
|
|
||||||
- name: Setup Docker buildx
|
- name: Setup Docker buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -320,7 +320,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: ./images
|
context: ./images
|
||||||
platforms: |
|
platforms: |
|
||||||
@@ -334,12 +334,11 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.source=${{github.server_url}}/${{github.repository}}
|
org.opencontainers.image.source=${{github.server_url}}/${{github.repository}}
|
||||||
org.opencontainers.image.licenses=MIT
|
|
||||||
annotations: |
|
|
||||||
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
|
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
|
||||||
|
org.opencontainers.image.licenses=MIT
|
||||||
|
|
||||||
- name: Generate attestation
|
- name: Generate attestation
|
||||||
uses: actions/attest-build-provenance@v4
|
uses: actions/attest-build-provenance@v3
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,5 +27,4 @@ TestResults
|
|||||||
TestLogs
|
TestLogs
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.mono
|
.mono
|
||||||
**/*.DotSettings.user
|
**/*.DotSettings.user
|
||||||
**/*.lscache
|
|
||||||
@@ -32,7 +32,7 @@ We are taking the following steps to better direct requests related to GitHub Ac
|
|||||||
|
|
||||||
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
|
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
|
||||||
|
|
||||||
3. Security Issues should be handled as per our [SECURITY.md](https://github.com/actions/runner?tab=security-ov-file)
|
3. Security Issues should be handled as per our [security.md](security.md)
|
||||||
|
|
||||||
We will still provide security updates for this project and fix major breaking changes during this time.
|
We will still provide security updates for this project and fix major breaking changes during this time.
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ The `installdependencies.sh` script should install all required dependencies on
|
|||||||
|
|
||||||
Debian based OS (Debian, Ubuntu, Linux Mint)
|
Debian based OS (Debian, Ubuntu, Linux Mint)
|
||||||
|
|
||||||
- liblttng-ust1t64, liblttng-ust1 or liblttng-ust0
|
- liblttng-ust1 or liblttng-ust0
|
||||||
- libkrb5-3
|
- libkrb5-3
|
||||||
- zlib1g
|
- zlib1g
|
||||||
- libssl3t64, libssl3, libssl1.1, libssl1.0.2 or libssl1.0.0
|
- libssl1.1, libssl1.0.2 or libssl1.0.0
|
||||||
- libicu80, libicu79, ..., libicu66, libicu65, libicu63, libicu60, libicu57, libicu55, or libicu52
|
- libicu63, libicu60, libicu57 or libicu55
|
||||||
|
|
||||||
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
|
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Source: https://github.com/dotnet/dotnet-docker
|
# Source: https://github.com/dotnet/dotnet-docker
|
||||||
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-noble AS build
|
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy AS build
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RUNNER_VERSION
|
ARG RUNNER_VERSION
|
||||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||||
ARG DOCKER_VERSION=29.5.3
|
ARG DOCKER_VERSION=29.0.1
|
||||||
ARG BUILDX_VERSION=0.34.1
|
ARG BUILDX_VERSION=0.30.0
|
||||||
|
|
||||||
RUN apt update -y && apt install curl unzip -y
|
RUN apt update -y && apt install curl unzip -y
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-c
|
|||||||
&& unzip ./runner-container-hooks.zip -d ./k8s \
|
&& unzip ./runner-container-hooks.zip -d ./k8s \
|
||||||
&& rm runner-container-hooks.zip
|
&& rm runner-container-hooks.zip
|
||||||
|
|
||||||
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v0.8.1/actions-runner-hooks-k8s-0.8.1.zip \
|
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v0.8.0/actions-runner-hooks-k8s-0.8.0.zip \
|
||||||
&& unzip ./runner-container-hooks.zip -d ./k8s-novolume \
|
&& unzip ./runner-container-hooks.zip -d ./k8s-novolume \
|
||||||
&& rm runner-container-hooks.zip
|
&& rm runner-container-hooks.zip
|
||||||
|
|
||||||
@@ -33,15 +33,15 @@ RUN export RUNNER_ARCH=${TARGETARCH} \
|
|||||||
&& rm -rf docker.tgz \
|
&& rm -rf docker.tgz \
|
||||||
&& mkdir -p /usr/local/lib/docker/cli-plugins \
|
&& mkdir -p /usr/local/lib/docker/cli-plugins \
|
||||||
&& curl -fLo /usr/local/lib/docker/cli-plugins/docker-buildx \
|
&& curl -fLo /usr/local/lib/docker/cli-plugins/docker-buildx \
|
||||||
"https://github.com/docker/buildx/releases/download/v${BUILDX_VERSION}/buildx-v${BUILDX_VERSION}.linux-${TARGETARCH}" \
|
"https://github.com/docker/buildx/releases/download/v${BUILDX_VERSION}/buildx-v${BUILDX_VERSION}.linux-${TARGETARCH}" \
|
||||||
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
|
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-buildx
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-noble
|
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV RUNNER_MANUALLY_TRAP_SIG=1
|
ENV RUNNER_MANUALLY_TRAP_SIG=1
|
||||||
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
|
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
|
||||||
ENV ImageOS=ubuntu24
|
ENV ImageOS=ubuntu22
|
||||||
|
|
||||||
# 'gpg-agent' and 'software-properties-common' are needed for the 'add-apt-repository' command that follows
|
# 'gpg-agent' and 'software-properties-common' are needed for the 'add-apt-repository' command that follows
|
||||||
RUN apt update -y \
|
RUN apt update -y \
|
||||||
@@ -54,6 +54,8 @@ RUN add-apt-repository ppa:git-core/ppa \
|
|||||||
&& apt install -y git \
|
&& apt install -y git \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /home/runner
|
||||||
|
|
||||||
RUN adduser --disabled-password --gecos "" --uid 1001 runner \
|
RUN adduser --disabled-password --gecos "" --uid 1001 runner \
|
||||||
&& groupadd docker --gid 123 \
|
&& groupadd docker --gid 123 \
|
||||||
&& usermod -aG sudo runner \
|
&& usermod -aG sudo runner \
|
||||||
@@ -62,8 +64,6 @@ RUN adduser --disabled-password --gecos "" --uid 1001 runner \
|
|||||||
&& echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers \
|
&& echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers \
|
||||||
&& chmod 777 /home/runner
|
&& chmod 777 /home/runner
|
||||||
|
|
||||||
WORKDIR /home/runner
|
|
||||||
|
|
||||||
COPY --chown=runner:docker --from=build /actions-runner .
|
COPY --chown=runner:docker --from=build /actions-runner .
|
||||||
COPY --from=build /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-buildx
|
COPY --from=build /usr/local/lib/docker/cli-plugins/docker-buildx /usr/local/lib/docker/cli-plugins/docker-buildx
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,30 @@
|
|||||||
## What's Changed
|
## What's Changed
|
||||||
* Bump System.ServiceProcess.ServiceController from 10.0.6 to 10.0.7 by @dependabot[bot] in https://github.com/actions/runner/pull/4370
|
* Custom Image: Preflight checks by @lawrencegripper in https://github.com/actions/runner/pull/4081
|
||||||
* Bump @actions/glob from 0.6.1 to 0.7.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4367
|
* Update dotnet sdk to latest version @8.0.415 by @github-actions[bot] in https://github.com/actions/runner/pull/4080
|
||||||
* feat: propagate actions dependencies by @nodeselector in https://github.com/actions/runner/pull/4372
|
* Link to an extant discussion category by @jsoref in https://github.com/actions/runner/pull/4084
|
||||||
* Not retry and report action download 403. by @TingluoHuang in https://github.com/actions/runner/pull/4391
|
* Improve logic around decide IsHostedServer. by @TingluoHuang in https://github.com/actions/runner/pull/4086
|
||||||
* Update setup job starting logs by @GitPaulo in https://github.com/actions/runner/pull/4383
|
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4093
|
||||||
* fix: expand commit hash regex to support SHA-256 (64-char) hashes by @yaananth in https://github.com/actions/runner/pull/4347
|
* Compare updated template evaluator by @ericsciple in https://github.com/actions/runner/pull/4092
|
||||||
* Move dap setup to setup job step by @rentziass in https://github.com/actions/runner/pull/4403
|
* fix(dockerfile): set more lenient permissions on /home/runner by @caxu-rh in https://github.com/actions/runner/pull/4083
|
||||||
* Add support for Ubuntu 26.04 (liblttng-ust1t64, libicu77-80) by @dvaldivia in https://github.com/actions/runner/pull/4394
|
* Add support for libicu73-76 for newer Debian/Ubuntu versions by @lets-build-an-ocean in https://github.com/actions/runner/pull/4098
|
||||||
* Update dotnet sdk to latest version @8.0.421 by @github-actions[bot] in https://github.com/actions/runner/pull/4428
|
* Bump actions/download-artifact from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4089
|
||||||
* Update Docker to v29.5.0 and Buildx to v0.34.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4425
|
* Bump actions/upload-artifact from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/4088
|
||||||
* Execute debugger REPL commands inside job container by @rentziass in https://github.com/actions/runner/pull/4420
|
* Bump Azure.Storage.Blobs from 12.25.1 to 12.26.0 by @dependabot[bot] in https://github.com/actions/runner/pull/4077
|
||||||
* Send welcome message in debugger console on connect by @rentziass in https://github.com/actions/runner/pull/4419
|
* Only start runner after network is online by @dupondje in https://github.com/actions/runner/pull/4094
|
||||||
* Update snapshot-if context and functions by @drielenr in https://github.com/actions/runner/pull/4443
|
* Retry http error related to DNS resolution failure. by @TingluoHuang in https://github.com/actions/runner/pull/4110
|
||||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4452
|
* Update Docker to v29.0.1 and Buildx to v0.30.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4114
|
||||||
* Allow disable node v8 maglev jit compiler on node24. by @TingluoHuang in https://github.com/actions/runner/pull/4447
|
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4115
|
||||||
* Update Node 24 default date to June 16th, 2026 by @salmanmkc in https://github.com/actions/runner/pull/4462
|
* Update dotnet sdk to latest version @8.0.416 by @github-actions[bot] in https://github.com/actions/runner/pull/4116
|
||||||
* Populate telemetry for non-action post-job steps by @drielenr in https://github.com/actions/runner/pull/4463
|
* Compare updated workflow parser for ActionManifestManager by @ericsciple in https://github.com/actions/runner/pull/4111
|
||||||
* Add SDK types and results plumbing for background step control by @lokesh755 in https://github.com/actions/runner/pull/4472
|
* Bump npm pkg version for hashFiles. by @TingluoHuang in https://github.com/actions/runner/pull/4122
|
||||||
* Add job execution view model by @rentziass in https://github.com/actions/runner/pull/4470
|
|
||||||
* Add thread-safety locks to StepsContext by @lokesh755 in https://github.com/actions/runner/pull/4475
|
|
||||||
* Add background step deferral infrastructure and metadata plumbing by @lokesh755 in https://github.com/actions/runner/pull/4479
|
|
||||||
* Wire job execution view into DAP by @rentziass in https://github.com/actions/runner/pull/4471
|
|
||||||
* Background steps execution engine by @lokesh755 in https://github.com/actions/runner/pull/4476
|
|
||||||
* Update Docker to v29.5.2 and Buildx to v0.34.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4451
|
|
||||||
* BrokerServer should not retry on 401. by @TingluoHuang in https://github.com/actions/runner/pull/4445
|
|
||||||
* Add new env var to allow single-prefix multiline logs on stdout by @nuclearpidgeon in https://github.com/actions/runner/pull/4424
|
|
||||||
* Bump Microsoft.DevTunnels.Connections from 1.3.39 to 1.3.48 by @dependabot[bot] in https://github.com/actions/runner/pull/4441
|
|
||||||
* Bump System.Formats.Asn1 and System.Security.Cryptography.Pkcs by @dependabot[bot] in https://github.com/actions/runner/pull/4369
|
|
||||||
|
|
||||||
## New Contributors
|
## New Contributors
|
||||||
* @GitPaulo made their first contribution in https://github.com/actions/runner/pull/4383
|
* @lawrencegripper made their first contribution in https://github.com/actions/runner/pull/4081
|
||||||
* @dvaldivia made their first contribution in https://github.com/actions/runner/pull/4394
|
* @caxu-rh made their first contribution in https://github.com/actions/runner/pull/4083
|
||||||
* @drielenr made their first contribution in https://github.com/actions/runner/pull/4443
|
* @lets-build-an-ocean made their first contribution in https://github.com/actions/runner/pull/4098
|
||||||
* @nuclearpidgeon made their first contribution in https://github.com/actions/runner/pull/4424
|
* @dupondje made their first contribution in https://github.com/actions/runner/pull/4094
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.334.0...v2.335.0
|
**Full Changelog**: https://github.com/actions/runner/compare/v2.329.0...v2.330.0
|
||||||
|
|
||||||
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
||||||
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<Update to ./src/runnerversion when creating release>
|
2.330.0
|
||||||
|
|||||||
1516
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
1516
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -32,20 +32,20 @@
|
|||||||
"author": "GitHub Actions",
|
"author": "GitHub Actions",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/glob": "^0.7.0"
|
"@actions/glob": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^3.1.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.59.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"@vercel/ncc": "^0.38.3",
|
"@vercel/ncc": "^0.38.3",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-github": "^4.10.2",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.4.0",
|
"lint-staged": "^15.5.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ NODE_URL=https://nodejs.org/dist
|
|||||||
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
|
NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
|
||||||
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
|
||||||
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
|
||||||
NODE20_VERSION="20.20.2"
|
NODE20_VERSION="20.19.5"
|
||||||
NODE24_VERSION="24.16.0"
|
NODE24_VERSION="24.11.1"
|
||||||
|
|
||||||
get_abs_path() {
|
get_abs_path() {
|
||||||
# exploits the fact that pwd will print abs path when no args
|
# exploits the fact that pwd will print abs path when no args
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ then
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
apt_get_with_fallbacks liblttng-ust1t64 liblttng-ust1 liblttng-ust0
|
apt_get_with_fallbacks liblttng-ust1 liblttng-ust0
|
||||||
if [ $? -ne 0 ]
|
if [ $? -ne 0 ]
|
||||||
then
|
then
|
||||||
echo "'$apt_get' failed with exit code '$?'"
|
echo "'$apt_get' failed with exit code '$?'"
|
||||||
@@ -102,7 +102,7 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
apt_get_with_fallbacks libssl3t64$ libssl3$ libssl1.1$ libssl1.0.2$ libssl1.0.0$
|
apt_get_with_fallbacks libssl1.1$ libssl1.0.2$ libssl1.0.0$
|
||||||
if [ $? -ne 0 ]
|
if [ $? -ne 0 ]
|
||||||
then
|
then
|
||||||
echo "'$apt_get' failed with exit code '$?'"
|
echo "'$apt_get' failed with exit code '$?'"
|
||||||
@@ -110,7 +110,7 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
apt_get_with_fallbacks libicu80 libicu79 libicu78 libicu77 libicu76 libicu75 libicu74 libicu73 libicu72 libicu71 libicu70 libicu69 libicu68 libicu67 libicu66 libicu65 libicu63 libicu60 libicu57 libicu55 libicu52
|
apt_get_with_fallbacks libicu76 libicu75 libicu74 libicu73 libicu72 libicu71 libicu70 libicu69 libicu68 libicu67 libicu66 libicu65 libicu63 libicu60 libicu57 libicu55 libicu52
|
||||||
if [ $? -ne 0 ]
|
if [ $? -ne 0 ]
|
||||||
then
|
then
|
||||||
echo "'$apt_get' failed with exit code '$?'"
|
echo "'$apt_get' failed with exit code '$?'"
|
||||||
|
|||||||
@@ -10,13 +10,6 @@ if %ERRORLEVEL% EQU 0 (
|
|||||||
exit /b 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 (
|
if %ERRORLEVEL% EQU 1 (
|
||||||
echo "Runner listener exit with terminated error, stop the service, no retry needed."
|
echo "Runner listener exit with terminated error, stop the service, no retry needed."
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|||||||
@@ -34,13 +34,11 @@ fi
|
|||||||
|
|
||||||
updateFile="update.finished"
|
updateFile="update.finished"
|
||||||
"$DIR"/bin/Runner.Listener run $*
|
"$DIR"/bin/Runner.Listener run $*
|
||||||
|
|
||||||
returnCode=$?
|
returnCode=$?
|
||||||
if [[ $returnCode == 0 ]]; then
|
if [[ $returnCode == 0 ]]; then
|
||||||
echo "Runner listener exit with 0 return code, stop the service, no retry needed."
|
echo "Runner listener exit with 0 return code, stop the service, no retry needed."
|
||||||
exit 0
|
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
|
elif [[ $returnCode == 1 ]]; then
|
||||||
echo "Runner listener exit with terminated error, stop the service, no retry needed."
|
echo "Runner listener exit with terminated error, stop the service, no retry needed."
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -25,14 +25,7 @@ call "%~dp0run-helper.cmd" %*
|
|||||||
if %ERRORLEVEL% EQU 1 (
|
if %ERRORLEVEL% EQU 1 (
|
||||||
echo "Restarting runner..."
|
echo "Restarting runner..."
|
||||||
goto :launch_helper
|
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
|
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ run() {
|
|||||||
returnCode=$?
|
returnCode=$?
|
||||||
if [[ $returnCode -eq 2 ]]; then
|
if [[ $returnCode -eq 2 ]]; then
|
||||||
echo "Restarting runner..."
|
echo "Restarting runner..."
|
||||||
elif [[ "$ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE" == "1" && $returnCode -eq 7 ]]; then
|
|
||||||
echo "Exiting runner..."
|
|
||||||
exit "$returnCode"
|
|
||||||
else
|
else
|
||||||
echo "Exiting runner..."
|
echo "Exiting runner..."
|
||||||
exit 0
|
exit 0
|
||||||
@@ -45,9 +42,6 @@ runWithManualTrap() {
|
|||||||
returnCode=$?
|
returnCode=$?
|
||||||
if [[ $returnCode -eq 2 ]]; then
|
if [[ $returnCode -eq 2 ]]; then
|
||||||
echo "Restarting runner..."
|
echo "Restarting runner..."
|
||||||
elif [[ "$ACTIONS_RUNNER_RETURN_VERSION_DEPRECATED_EXIT_CODE" == "1" && $returnCode -eq 7 ]]; then
|
|
||||||
echo "Exiting runner..."
|
|
||||||
exit "$returnCode"
|
|
||||||
else
|
else
|
||||||
echo "Exiting runner..."
|
echo "Exiting runner..."
|
||||||
# Unregister signal handling before exit
|
# Unregister signal handling before exit
|
||||||
|
|||||||
@@ -1,36 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# try to use sleep if available
|
|
||||||
if [ -x "$(command -v sleep)" ]; then
|
|
||||||
sleep "$1"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# try to use ping if available
|
|
||||||
if [ -x "$(command -v ping)" ]; then
|
|
||||||
ping -c $(( $1 + 1 )) 127.0.0.1 > /dev/null
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# try to use read -t from stdin/stdout/stderr if we are in bash
|
|
||||||
if [ -n "$BASH_VERSION" ]; then
|
|
||||||
if command -v read >/dev/null 2>&1; then
|
|
||||||
if [ -t 0 ]; then
|
|
||||||
read -t "$1" -u 0 || :;
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
if [ -t 1 ]; then
|
|
||||||
read -t "$1" -u 1 || :;
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
if [ -t 2 ]; then
|
|
||||||
read -t "$1" -u 2 || :;
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# fallback to a busy wait
|
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
while [[ $SECONDS -lt $1 ]]; do
|
while [[ $SECONDS -lt $1 ]]; do
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -204,26 +204,6 @@ namespace GitHub.Runner.Common
|
|||||||
return unescaped;
|
return unescaped;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Escapes special characters in a value using the standard action command escape mappings.
|
|
||||||
/// Iterates in reverse so that '%' is escaped first to avoid double-encoding.
|
|
||||||
/// </summary>
|
|
||||||
public static string EscapeValue(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
string escaped = value;
|
|
||||||
for (int i = _escapeMappings.Length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
escaped = escaped.Replace(_escapeMappings[i].Token, _escapeMappings[i].Replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return escaped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string UnescapeProperty(string escaped)
|
private static string UnescapeProperty(string escaped)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(escaped))
|
if (string.IsNullOrEmpty(escaped))
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public bool ShouldRetryException(Exception ex)
|
public bool ShouldRetryException(Exception ex)
|
||||||
{
|
{
|
||||||
if (ex is AccessDeniedException || ex is VssUnauthorizedException || ex is RunnerNotFoundException || ex is HostedRunnerDeprovisionedException)
|
if (ex is AccessDeniedException || ex is RunnerNotFoundException || ex is HostedRunnerDeprovisionedException)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,41 +75,6 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
return UrlUtil.IsHostedServer(new UriBuilder(GitHubUrl));
|
return UrlUtil.IsHostedServer(new UriBuilder(GitHubUrl));
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// feature flag env in case the new logic is wrong.
|
|
||||||
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_FORCE_EMPTY_GITHUB_URL_IS_HOSTED")))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GitHubUrl will be empty for jit configured runner
|
|
||||||
// We will try to infer it from the ServerUrl/ServerUrlV2
|
|
||||||
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_FORCE_GHES")))
|
|
||||||
{
|
|
||||||
// Allow env to override and force GHES in case the inference logic is wrong.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ServerUrl))
|
|
||||||
{
|
|
||||||
// pipelines services
|
|
||||||
var serverUrl = new UriBuilder(ServerUrl);
|
|
||||||
return serverUrl.Host.EndsWith(".actions.githubusercontent.com", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| serverUrl.Host.EndsWith(".codedev.ms", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ServerUrlV2))
|
|
||||||
{
|
|
||||||
// broker-listener
|
|
||||||
var serverUrlV2 = new UriBuilder(ServerUrlV2);
|
|
||||||
return serverUrlV2.Host.EndsWith(".actions.githubusercontent.com", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| serverUrlV2.Host.EndsWith(".githubapp.com", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| serverUrlV2.Host.EndsWith(".ghe.com", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| serverUrlV2.Host.EndsWith(".actions.localhost", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| serverUrlV2.Host.EndsWith(".ghe.localhost", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to true since Hosted runners likely don't have this property set.
|
// Default to true since Hosted runners likely don't have this property set.
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// the runner is migrated to runner admin.
|
||||||
public const int RunnerConfigurationRefreshed = 6;
|
public const int RunnerConfigurationRefreshed = 6;
|
||||||
public const int RunnerVersionDeprecated = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Features
|
public static class Features
|
||||||
@@ -170,50 +169,26 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
|
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
|
||||||
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
|
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
|
||||||
public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors";
|
public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors";
|
||||||
|
public static readonly string ContainerActionRunnerTemp = "actions_container_action_runner_temp";
|
||||||
public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_check";
|
public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_check";
|
||||||
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
|
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
|
||||||
public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser";
|
public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser";
|
||||||
public static readonly string ServiceContainerCommand = "actions_service_container_command";
|
|
||||||
public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions";
|
|
||||||
public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations";
|
|
||||||
public static readonly string EmitCompositeMarkers = "actions_runner_emit_composite_markers";
|
|
||||||
public static readonly string BatchActionResolution = "actions_batch_action_resolution";
|
|
||||||
public static readonly string UseBearerTokenForCodeload = "actions_use_bearer_token_for_codeload";
|
|
||||||
public static readonly string OverrideDebuggerWelcomeMessage = "actions_runner_override_debugger_welcome_message";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node version migration related constants
|
// Node version migration related constants
|
||||||
public static class NodeMigration
|
public static class NodeMigration
|
||||||
{
|
{
|
||||||
// Node versions
|
// Node versions
|
||||||
public static readonly string Node20 = "node20";
|
public static readonly string Node20 = "node20";
|
||||||
public static readonly string Node24 = "node24";
|
public static readonly string Node24 = "node24";
|
||||||
|
|
||||||
// Environment variables for controlling node version selection
|
// Environment variables for controlling node version selection
|
||||||
public static readonly string ForceNode24Variable = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24";
|
public static readonly string ForceNode24Variable = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24";
|
||||||
public static readonly string AllowUnsecureNodeVersionVariable = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
|
public static readonly string AllowUnsecureNodeVersionVariable = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
|
||||||
|
|
||||||
// Feature flags for controlling the migration phases
|
// Feature flags for controlling the migration phases
|
||||||
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
|
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
|
||||||
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";
|
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 16th, 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";
|
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
||||||
@@ -295,7 +270,6 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string AllowUnsupportedCommands = "ACTIONS_ALLOW_UNSECURE_COMMANDS";
|
public static readonly string AllowUnsupportedCommands = "ACTIONS_ALLOW_UNSECURE_COMMANDS";
|
||||||
public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS";
|
public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS";
|
||||||
public static readonly string RequireJobContainer = "ACTIONS_RUNNER_REQUIRE_JOB_CONTAINER";
|
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 RunnerDebug = "ACTIONS_RUNNER_DEBUG";
|
||||||
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
|
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
|
||||||
}
|
}
|
||||||
@@ -308,10 +282,7 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string ForcedInternalNodeVersion = "ACTIONS_RUNNER_FORCED_INTERNAL_NODE_VERSION";
|
public static readonly string ForcedInternalNodeVersion = "ACTIONS_RUNNER_FORCED_INTERNAL_NODE_VERSION";
|
||||||
public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
|
public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
|
||||||
public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT";
|
public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT";
|
||||||
public static readonly string DisableStdoutMultilineLogPrefixing = "ACTIONS_RUNNER_DISABLE_STDOUT_MULTILINE_LOG_PREFIXING";
|
|
||||||
public static readonly string ActionArchiveCacheDirectory = "ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE";
|
public static readonly string ActionArchiveCacheDirectory = "ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE";
|
||||||
public static readonly string SymlinkCachedActions = "ACTIONS_RUNNER_SYMLINK_CACHED_ACTIONS";
|
|
||||||
public static readonly string EmitCompositeMarkers = "ACTIONS_RUNNER_EMIT_COMPOSITE_MARKERS";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class System
|
public static class System
|
||||||
|
|||||||
@@ -837,15 +837,6 @@ namespace GitHub.Runner.Common
|
|||||||
timelineRecord.Variables[variable.Key] = variable.Value.Clone();
|
timelineRecord.Variables[variable.Key] = variable.Value.Clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge background step metadata
|
|
||||||
if (rec.IsBackground)
|
|
||||||
{
|
|
||||||
timelineRecord.IsBackground = rec.IsBackground;
|
|
||||||
}
|
|
||||||
timelineRecord.BackgroundControlType = rec.BackgroundControlType ?? timelineRecord.BackgroundControlType;
|
|
||||||
timelineRecord.BackgroundControlStepIds = rec.BackgroundControlStepIds ?? timelineRecord.BackgroundControlStepIds;
|
|
||||||
timelineRecord.ParallelGroupId = rec.ParallelGroupId ?? timelineRecord.ParallelGroupId;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.3" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||||
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
|
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -9,12 +9,10 @@ namespace GitHub.Runner.Common
|
|||||||
public sealed class StdoutTraceListener : ConsoleTraceListener
|
public sealed class StdoutTraceListener : ConsoleTraceListener
|
||||||
{
|
{
|
||||||
private readonly string _hostType;
|
private readonly string _hostType;
|
||||||
private readonly bool _disablePrefixMultilineLogs = false;
|
|
||||||
|
|
||||||
public StdoutTraceListener(string hostType)
|
public StdoutTraceListener(string hostType)
|
||||||
{
|
{
|
||||||
this._hostType = hostType;
|
this._hostType = hostType;
|
||||||
this._disablePrefixMultilineLogs = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Agent.DisableStdoutMultilineLogPrefixing));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copied and modified slightly from .Net Core source code. Modification was required to make it compile.
|
// Copied and modified slightly from .Net Core source code. Modification was required to make it compile.
|
||||||
@@ -28,20 +26,11 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(message))
|
if (!string.IsNullOrEmpty(message))
|
||||||
{
|
{
|
||||||
if (!this._disablePrefixMultilineLogs)
|
var messageLines = message.Split(Environment.NewLine);
|
||||||
{
|
foreach (var messageLine in messageLines)
|
||||||
var messageLines = message.Split(Environment.NewLine);
|
|
||||||
foreach (var messageLine in messageLines)
|
|
||||||
{
|
|
||||||
WriteHeader(source, eventType, id);
|
|
||||||
WriteLine(messageLine);
|
|
||||||
WriteFooter(eventCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
WriteHeader(source, eventType, id);
|
WriteHeader(source, eventType, id);
|
||||||
WriteLine(message);
|
WriteLine(messageLine);
|
||||||
WriteFooter(eventCache);
|
WriteFooter(eventCache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,6 @@ namespace GitHub.Runner.Common
|
|||||||
private ISecretMasker _secretMasker;
|
private ISecretMasker _secretMasker;
|
||||||
private TraceSource _traceSource;
|
private TraceSource _traceSource;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The underlying <see cref="System.Diagnostics.TraceSource"/> for this instance.
|
|
||||||
/// Useful when third-party libraries require a <see cref="System.Diagnostics.TraceSource"/>
|
|
||||||
/// to route their diagnostics into the runner's log infrastructure.
|
|
||||||
/// </summary>
|
|
||||||
public TraceSource Source => _traceSource;
|
|
||||||
|
|
||||||
public Tracing(string name, ISecretMasker secretMasker, SourceSwitch sourceSwitch, HostTraceListener traceListener, StdoutTraceListener stdoutTraceListener = null)
|
public Tracing(string name, ISecretMasker secretMasker, SourceSwitch sourceSwitch, HostTraceListener traceListener, StdoutTraceListener stdoutTraceListener = null)
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(secretMasker, nameof(secretMasker));
|
ArgUtil.NotNull(secretMasker, nameof(secretMasker));
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace GitHub.Runner.Common.Util
|
|||||||
{
|
{
|
||||||
return (Constants.Runner.NodeMigration.Node24, null);
|
return (Constants.Runner.NodeMigration.Node24, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get environment variable details with source information
|
// Get environment variable details with source information
|
||||||
var forceNode24Details = GetEnvironmentVariableDetails(
|
var forceNode24Details = GetEnvironmentVariableDetails(
|
||||||
Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment);
|
Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment);
|
||||||
@@ -108,50 +108,14 @@ namespace GitHub.Runner.Common.Util
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="preferredVersion">The preferred Node version</param>
|
/// <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>
|
/// <returns>A tuple containing the adjusted node version and an optional warning message</returns>
|
||||||
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(
|
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
|
||||||
string preferredVersion,
|
|
||||||
bool deprecateArm32 = false,
|
|
||||||
bool killArm32 = false,
|
|
||||||
string node20RemovalDate = null)
|
|
||||||
{
|
{
|
||||||
bool isArm32Linux = Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
|
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) &&
|
||||||
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux);
|
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))
|
|
||||||
{
|
{
|
||||||
return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
|
return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,12 +178,8 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate can connect using the obtained vss credentials.
|
// Validate can connect.
|
||||||
// In Runner Admin flow there's nothing new to test connection to at this point as registerToken is already validated via GetTenantCredential.
|
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), creds);
|
||||||
if (!runnerSettings.UseRunnerAdminFlow)
|
|
||||||
{
|
|
||||||
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), creds);
|
|
||||||
}
|
|
||||||
|
|
||||||
_term.WriteLine();
|
_term.WriteLine();
|
||||||
_term.WriteSuccessMessage("Connected to GitHub");
|
_term.WriteSuccessMessage("Connected to GitHub");
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace GitHub.Runner.Listener
|
|||||||
public interface IJobDispatcher : IRunnerService
|
public interface IJobDispatcher : IRunnerService
|
||||||
{
|
{
|
||||||
bool Busy { get; }
|
bool Busy { get; }
|
||||||
TaskCompletionSource<TaskResult> RunOnceJobCompleted { get; }
|
TaskCompletionSource<bool> RunOnceJobCompleted { get; }
|
||||||
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
|
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
|
||||||
bool Cancel(JobCancelMessage message);
|
bool Cancel(JobCancelMessage message);
|
||||||
Task WaitAsync(CancellationToken token);
|
Task WaitAsync(CancellationToken token);
|
||||||
@@ -56,7 +56,7 @@ namespace GitHub.Runner.Listener
|
|||||||
// timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT
|
// timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT
|
||||||
private TimeSpan _channelTimeout;
|
private TimeSpan _channelTimeout;
|
||||||
|
|
||||||
private TaskCompletionSource<TaskResult> _runOnceJobCompleted = new();
|
private TaskCompletionSource<bool> _runOnceJobCompleted = new();
|
||||||
|
|
||||||
public event EventHandler<JobStatusEventArgs> JobStatus;
|
public event EventHandler<JobStatusEventArgs> JobStatus;
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Info($"Set runner/worker IPC timeout to {_channelTimeout.TotalSeconds} seconds.");
|
Trace.Info($"Set runner/worker IPC timeout to {_channelTimeout.TotalSeconds} seconds.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public TaskCompletionSource<TaskResult> RunOnceJobCompleted => _runOnceJobCompleted;
|
public TaskCompletionSource<bool> RunOnceJobCompleted => _runOnceJobCompleted;
|
||||||
|
|
||||||
public bool Busy { get; private set; }
|
public bool Busy { get; private set; }
|
||||||
|
|
||||||
@@ -340,19 +340,18 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||||
{
|
{
|
||||||
var jobResult = TaskResult.Succeeded;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
jobResult = await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
|
await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Trace.Info("Fire signal for one time used runner.");
|
Trace.Info("Fire signal for one time used runner.");
|
||||||
_runOnceJobCompleted.TrySetResult(jobResult);
|
_runOnceJobCompleted.TrySetResult(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
private async Task RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
|
||||||
{
|
{
|
||||||
Busy = true;
|
Busy = true;
|
||||||
try
|
try
|
||||||
@@ -400,7 +399,7 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
// renew job request task complete means we run out of retry for the first job request renew.
|
// renew job request task complete means we run out of retry for the first job request renew.
|
||||||
Trace.Info($"Unable to renew job request for job {message.JobId} for the first time, stop dispatching job to worker.");
|
Trace.Info($"Unable to renew job request for job {message.JobId} for the first time, stop dispatching job to worker.");
|
||||||
return TaskResult.Abandoned;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jobRequestCancellationToken.IsCancellationRequested)
|
if (jobRequestCancellationToken.IsCancellationRequested)
|
||||||
@@ -413,7 +412,7 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
// complete job request with result Cancelled
|
// complete job request with result Cancelled
|
||||||
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
|
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
|
||||||
return TaskResult.Canceled;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HostContext.WritePerfCounter($"JobRequestRenewed_{requestId.ToString()}");
|
HostContext.WritePerfCounter($"JobRequestRenewed_{requestId.ToString()}");
|
||||||
@@ -524,7 +523,7 @@ namespace GitHub.Runner.Listener
|
|||||||
await renewJobRequest;
|
await renewJobRequest;
|
||||||
|
|
||||||
// not finish the job request since the job haven't run on worker at all, we will not going to set a result to server.
|
// not finish the job request since the job haven't run on worker at all, we will not going to set a result to server.
|
||||||
return TaskResult.Failed;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we get first jobrequest renew succeed and start the worker process with the job message.
|
// we get first jobrequest renew succeed and start the worker process with the job message.
|
||||||
@@ -605,7 +604,7 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Error(detailInfo);
|
Trace.Error(detailInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TaskResultUtil.TranslateFromReturnCode(returnCode);
|
return;
|
||||||
}
|
}
|
||||||
else if (completedTask == renewJobRequest)
|
else if (completedTask == renewJobRequest)
|
||||||
{
|
{
|
||||||
@@ -707,8 +706,6 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
// complete job request
|
// complete job request
|
||||||
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel);
|
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel);
|
||||||
|
|
||||||
return resultOnAbandonOrCancel;
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -141,9 +141,9 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
catch (AccessDeniedException e) when (e.ErrorCode == 1)
|
catch (AccessDeniedException e) when (e.ErrorCode == 1)
|
||||||
{
|
{
|
||||||
terminal.WriteError($"An error occurred: {e.Message}");
|
terminal.WriteError($"An error occured: {e.Message}");
|
||||||
trace.Error(e);
|
trace.Error(e);
|
||||||
return GetRunnerVersionDeprecatedExitCode();
|
return Constants.Runner.ReturnCode.TerminatedError;
|
||||||
}
|
}
|
||||||
catch (RunnerNotFoundException e)
|
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()
|
private static void LoadAndSetEnv()
|
||||||
{
|
{
|
||||||
var binDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
var binDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.7" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -324,11 +324,8 @@ namespace GitHub.Runner.Listener
|
|||||||
HostContext.EnableAuthMigration("EnableAuthMigrationByDefault");
|
HostContext.EnableAuthMigration("EnableAuthMigrationByDefault");
|
||||||
}
|
}
|
||||||
|
|
||||||
// hosted runner only run one job and would like to know the result of the job for telemetry and alerting on failure spike.
|
|
||||||
var returnJobResultForHosted = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED"));
|
|
||||||
|
|
||||||
// Run the runner interactively or as service
|
// Run the runner interactively or as service
|
||||||
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral || returnJobResultForHosted, returnJobResultForHosted);
|
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -404,32 +401,17 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
|
|
||||||
//create worker manager, create message listener and start listening to the queue
|
//create worker manager, create message listener and start listening to the queue
|
||||||
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false, bool returnRunOnceJobResult = false)
|
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Trace.Info(nameof(RunAsync));
|
Trace.Info(nameof(RunAsync));
|
||||||
|
|
||||||
// Validate directory permissions.
|
|
||||||
string workDirectory = HostContext.GetDirectory(WellKnownDirectory.Work);
|
|
||||||
Trace.Info($"Validating directory permissions for: '{workDirectory}'");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(workDirectory);
|
|
||||||
IOUtil.ValidateExecutePermission(workDirectory);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Error(ex);
|
|
||||||
_term.WriteError($"Fail to create and validate runner's work directory '{workDirectory}'.");
|
|
||||||
return Constants.Runner.ReturnCode.TerminatedError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First try using migrated settings if available
|
// First try using migrated settings if available
|
||||||
var configManager = HostContext.GetService<IConfigurationManager>();
|
var configManager = HostContext.GetService<IConfigurationManager>();
|
||||||
RunnerSettings migratedSettings = null;
|
RunnerSettings migratedSettings = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
migratedSettings = configManager.LoadMigratedSettings();
|
migratedSettings = configManager.LoadMigratedSettings();
|
||||||
Trace.Info("Loaded migrated settings from .runner_migrated file");
|
Trace.Info("Loaded migrated settings from .runner_migrated file");
|
||||||
@@ -440,15 +422,15 @@ namespace GitHub.Runner.Listener
|
|||||||
// If migrated settings file doesn't exist or can't be loaded, we'll use the provided settings
|
// If migrated settings file doesn't exist or can't be loaded, we'll use the provided settings
|
||||||
Trace.Info($"Failed to load migrated settings: {ex.Message}");
|
Trace.Info($"Failed to load migrated settings: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool usedMigratedSettings = false;
|
bool usedMigratedSettings = false;
|
||||||
|
|
||||||
if (migratedSettings != null)
|
if (migratedSettings != null)
|
||||||
{
|
{
|
||||||
// Try to create session with migrated settings first
|
// Try to create session with migrated settings first
|
||||||
Trace.Info("Attempting to create session using migrated settings");
|
Trace.Info("Attempting to create session using migrated settings");
|
||||||
_listener = GetMessageListener(migratedSettings, isMigratedSettings: true);
|
_listener = GetMessageListener(migratedSettings, isMigratedSettings: true);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
|
||||||
@@ -468,7 +450,7 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Error($"Exception when creating session with migrated settings: {ex}");
|
Trace.Error($"Exception when creating session with migrated settings: {ex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If migrated settings weren't used or session creation failed, use original settings
|
// If migrated settings weren't used or session creation failed, use original settings
|
||||||
if (!usedMigratedSettings)
|
if (!usedMigratedSettings)
|
||||||
{
|
{
|
||||||
@@ -521,7 +503,7 @@ namespace GitHub.Runner.Listener
|
|||||||
restartSession = true;
|
restartSession = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskAgentMessage message = null;
|
TaskAgentMessage message = null;
|
||||||
bool skipMessageDeletion = false;
|
bool skipMessageDeletion = false;
|
||||||
try
|
try
|
||||||
@@ -583,21 +565,6 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Info($"Ignore any exception after cancel message loop. {ex}");
|
Trace.Info($"Ignore any exception after cancel message loop. {ex}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnRunOnceJobResult)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var jobResult = await jobDispatcher.RunOnceJobCompleted.Task;
|
|
||||||
return TaskResultUtil.TranslateToReturnCode(jobResult);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Error("run once job finished with error.");
|
|
||||||
Trace.Error(ex);
|
|
||||||
return Constants.Runner.ReturnCode.TerminatedError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Constants.Runner.ReturnCode.Success;
|
return Constants.Runner.ReturnCode.Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -884,15 +851,15 @@ namespace GitHub.Runner.Listener
|
|||||||
return Constants.Runner.ReturnCode.Success;
|
return Constants.Runner.ReturnCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce, bool returnRunOnceJobResult)
|
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce)
|
||||||
{
|
{
|
||||||
int returnCode = Constants.Runner.ReturnCode.Success;
|
int returnCode = Constants.Runner.ReturnCode.Success;
|
||||||
bool restart = false;
|
bool restart = false;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
restart = false;
|
restart = false;
|
||||||
returnCode = await RunAsync(settings, runOnce, returnRunOnceJobResult);
|
returnCode = await RunAsync(settings, runOnce);
|
||||||
|
|
||||||
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
|
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
|
||||||
{
|
{
|
||||||
Trace.Info("Runner configuration was refreshed, restarting session...");
|
Trace.Info("Runner configuration was refreshed, restarting session...");
|
||||||
|
|||||||
@@ -120,10 +120,8 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Trace.Error(ex);
|
|
||||||
_terminal.WriteError($"Runner update failed: {ex.Message}");
|
|
||||||
_updateTrace.Enqueue(ex.ToString());
|
_updateTrace.Enqueue(ex.ToString());
|
||||||
return false;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -120,10 +120,8 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Trace.Error(ex);
|
|
||||||
_terminal.WriteError($"Runner update failed: {ex.Message}");
|
|
||||||
_updateTrace.Enqueue(ex.ToString());
|
_updateTrace.Enqueue(ex.ToString());
|
||||||
return false;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
{
|
{
|
||||||
public class CheckoutTask : IRunnerActionPlugin
|
public class CheckoutTask : IRunnerActionPlugin
|
||||||
{
|
{
|
||||||
|
private readonly Regex _validSha1 = new(@"\b[0-9a-f]{40}\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
public async Task RunAsync(RunnerActionPluginExecutionContext executionContext, CancellationToken token)
|
public async Task RunAsync(RunnerActionPluginExecutionContext executionContext, CancellationToken token)
|
||||||
{
|
{
|
||||||
string runnerWorkspace = executionContext.GetRunnerContext("workspace");
|
string runnerWorkspace = executionContext.GetRunnerContext("workspace");
|
||||||
@@ -97,7 +99,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
|
|||||||
{
|
{
|
||||||
sourceBranch = refInput;
|
sourceBranch = refInput;
|
||||||
sourceVersion = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Version); // version get removed when checkout move to repo in the graph
|
sourceVersion = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Version); // version get removed when checkout move to repo in the graph
|
||||||
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.CommitHash))
|
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.SHA1))
|
||||||
{
|
{
|
||||||
sourceVersion = sourceBranch;
|
sourceVersion = sourceBranch;
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
|
|||||||
{
|
{
|
||||||
sourceBranch = refInput;
|
sourceBranch = refInput;
|
||||||
sourceVersion = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Version); // version get removed when checkout move to repo in the graph
|
sourceVersion = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Version); // version get removed when checkout move to repo in the graph
|
||||||
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.CommitHash))
|
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.SHA1))
|
||||||
{
|
{
|
||||||
sourceVersion = sourceBranch;
|
sourceVersion = sourceBranch;
|
||||||
// If Ref is a SHA and the repo is self, we need to use github.ref as source branch since it might be refs/pull/*
|
// If Ref is a SHA and the repo is self, we need to use github.ref as source branch since it might be refs/pull/*
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.9" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
|
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
|||||||
@@ -93,16 +93,6 @@ namespace GitHub.Runner.Sdk
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FileSystemInfo CreateSymbolicLink(string destDirectory, string srcDirectory)
|
|
||||||
{
|
|
||||||
// ensure directory chain exists
|
|
||||||
Directory.CreateDirectory(destDirectory);
|
|
||||||
// delete leaf directory
|
|
||||||
Directory.Delete(destDirectory);
|
|
||||||
// create symlink for the leaf directory
|
|
||||||
return Directory.CreateSymbolicLink(destDirectory, srcDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Delete(string path, CancellationToken cancellationToken)
|
public static void Delete(string path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
DeleteDirectory(path, cancellationToken);
|
DeleteDirectory(path, cancellationToken);
|
||||||
|
|||||||
@@ -282,15 +282,8 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.DeferredEnvironmentVariables != null)
|
context.Global.EnvironmentVariables[envName] = command.Data;
|
||||||
{
|
context.SetEnvContext(envName, command.Data);
|
||||||
context.DeferredEnvironmentVariables[envName] = command.Data;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Global.EnvironmentVariables[envName] = command.Data;
|
|
||||||
context.SetEnvContext(envName, command.Data);
|
|
||||||
}
|
|
||||||
context.Debug($"{envName}='{command.Data}'");
|
context.Debug($"{envName}='{command.Data}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,31 +318,13 @@ namespace GitHub.Runner.Worker
|
|||||||
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.Global.HasDeprecatedSetOutput)
|
|
||||||
{
|
|
||||||
context.Global.HasDeprecatedSetOutput = true;
|
|
||||||
var telemetry = new JobTelemetry
|
|
||||||
{
|
|
||||||
Type = JobTelemetryType.ActionCommand,
|
|
||||||
Message = "DeprecatedCommand: set-output"
|
|
||||||
};
|
|
||||||
context.Global.JobTelemetry.Add(telemetry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
|
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
|
||||||
{
|
{
|
||||||
throw new Exception("Required field 'name' is missing in ##[set-output] command.");
|
throw new Exception("Required field 'name' is missing in ##[set-output] command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.DeferredOutputs != null)
|
context.SetOutput(outputName, command.Data, out var reference);
|
||||||
{
|
context.Debug($"{reference}='{command.Data}'");
|
||||||
context.DeferredOutputs[outputName] = command.Data;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.SetOutput(outputName, command.Data, out var reference);
|
|
||||||
context.Debug($"{reference}='{command.Data}'");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class SetOutputCommandProperties
|
private static class SetOutputCommandProperties
|
||||||
@@ -378,17 +353,6 @@ namespace GitHub.Runner.Worker
|
|||||||
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.Global.HasDeprecatedSaveState)
|
|
||||||
{
|
|
||||||
context.Global.HasDeprecatedSaveState = true;
|
|
||||||
var telemetry = new JobTelemetry
|
|
||||||
{
|
|
||||||
Type = JobTelemetryType.ActionCommand,
|
|
||||||
Message = "DeprecatedCommand: save-state"
|
|
||||||
};
|
|
||||||
context.Global.JobTelemetry.Add(telemetry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
|
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
|
||||||
{
|
{
|
||||||
throw new Exception("Required field 'name' is missing in ##[save-state] command.");
|
throw new Exception("Required field 'name' is missing in ##[save-state] command.");
|
||||||
@@ -479,16 +443,8 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArgUtil.NotNullOrEmpty(command.Data, "path");
|
ArgUtil.NotNullOrEmpty(command.Data, "path");
|
||||||
if (context.DeferredPrependPath != null)
|
context.Global.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
|
||||||
{
|
context.Global.PrependPath.Add(command.Data);
|
||||||
context.DeferredPrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
|
|
||||||
context.DeferredPrependPath.Add(command.Data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Global.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
|
|
||||||
context.Global.PrependPath.Add(command.Data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,13 +79,6 @@ namespace GitHub.Runner.Worker
|
|||||||
PreStepTracker = new Dictionary<Guid, IActionRunner>()
|
PreStepTracker = new Dictionary<Guid, IActionRunner>()
|
||||||
};
|
};
|
||||||
var containerSetupSteps = new List<JobExtensionRunner>();
|
var containerSetupSteps = new List<JobExtensionRunner>();
|
||||||
var batchActionResolution = (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.BatchActionResolution) ?? false)
|
|
||||||
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION"));
|
|
||||||
// Stack-local cache: same action (owner/repo@ref) is resolved only once,
|
|
||||||
// even if it appears at multiple depths in a composite tree.
|
|
||||||
var resolvedDownloadInfos = batchActionResolution
|
|
||||||
? new Dictionary<string, WebApi.ActionDownloadInfo>(StringComparer.Ordinal)
|
|
||||||
: null;
|
|
||||||
var depth = 0;
|
var depth = 0;
|
||||||
// We are running at the start of a job
|
// We are running at the start of a job
|
||||||
if (rootStepId == default(Guid))
|
if (rootStepId == default(Guid))
|
||||||
@@ -112,23 +105,13 @@ namespace GitHub.Runner.Worker
|
|||||||
PrepareActionsState result = new PrepareActionsState();
|
PrepareActionsState result = new PrepareActionsState();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result = batchActionResolution
|
result = await PrepareActionsRecursiveAsync(executionContext, state, actions, depth, rootStepId);
|
||||||
? await PrepareActionsRecursiveAsync(executionContext, state, actions, resolvedDownloadInfos, depth, rootStepId)
|
|
||||||
: await PrepareActionsRecursiveLegacyAsync(executionContext, state, actions, depth, rootStepId);
|
|
||||||
}
|
}
|
||||||
catch (FailedToResolveActionDownloadInfoException ex)
|
catch (FailedToResolveActionDownloadInfoException ex)
|
||||||
{
|
{
|
||||||
// Log the error and fail the PrepareActionsAsync Initialization.
|
// Log the error and fail the PrepareActionsAsync Initialization.
|
||||||
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
||||||
executionContext.InfrastructureError(ex.InnerException?.Message ?? ex.Message, category: "resolve_action");
|
executionContext.InfrastructureError(ex.Message, category: "resolve_action");
|
||||||
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;
|
executionContext.Result = TaskResult.Failed;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -178,192 +161,7 @@ namespace GitHub.Runner.Worker
|
|||||||
return new PrepareResult(containerSetupSteps, result.PreStepTracker);
|
return new PrepareResult(containerSetupSteps, result.PreStepTracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PrepareActionsState> PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Dictionary<string, WebApi.ActionDownloadInfo> resolvedDownloadInfos, Int32 depth = 0, Guid parentStepId = default(Guid))
|
private async Task<PrepareActionsState> PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Int32 depth = 0, Guid parentStepId = default(Guid))
|
||||||
{
|
|
||||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
||||||
if (depth > Constants.CompositeActionsMaxDepth)
|
|
||||||
{
|
|
||||||
throw new Exception($"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var repositoryActions = new List<Pipelines.ActionStep>();
|
|
||||||
|
|
||||||
foreach (var action in actions)
|
|
||||||
{
|
|
||||||
if (action.Reference.Type == Pipelines.ActionSourceType.ContainerRegistry)
|
|
||||||
{
|
|
||||||
ArgUtil.NotNull(action, nameof(action));
|
|
||||||
var containerReference = action.Reference as Pipelines.ContainerRegistryReference;
|
|
||||||
ArgUtil.NotNull(containerReference, nameof(containerReference));
|
|
||||||
ArgUtil.NotNullOrEmpty(containerReference.Image, nameof(containerReference.Image));
|
|
||||||
|
|
||||||
if (!state.ImagesToPull.ContainsKey(containerReference.Image))
|
|
||||||
{
|
|
||||||
state.ImagesToPull[containerReference.Image] = new List<Guid>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace.Info($"Action {action.Name} ({action.Id}) needs to pull image '{containerReference.Image}'");
|
|
||||||
state.ImagesToPull[containerReference.Image].Add(action.Id);
|
|
||||||
}
|
|
||||||
else if (action.Reference.Type == Pipelines.ActionSourceType.Repository)
|
|
||||||
{
|
|
||||||
repositoryActions.Add(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (repositoryActions.Count > 0)
|
|
||||||
{
|
|
||||||
// Resolve download info, skipping any actions already cached.
|
|
||||||
await ResolveNewActionsAsync(executionContext, repositoryActions, resolvedDownloadInfos);
|
|
||||||
|
|
||||||
// Download each action.
|
|
||||||
foreach (var action in repositoryActions)
|
|
||||||
{
|
|
||||||
var lookupKey = GetDownloadInfoLookupKey(action);
|
|
||||||
if (string.IsNullOrEmpty(lookupKey))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!resolvedDownloadInfos.TryGetValue(lookupKey, out var downloadInfo))
|
|
||||||
{
|
|
||||||
throw new Exception($"Missing download info for {lookupKey}");
|
|
||||||
}
|
|
||||||
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse action.yml and collect composite sub-actions for batched
|
|
||||||
// resolution below. Pre/post step registration is deferred until
|
|
||||||
// after recursion so that HasPre/HasPost reflect the full subtree.
|
|
||||||
var nextLevel = new List<(Pipelines.ActionStep action, Guid parentId)>();
|
|
||||||
|
|
||||||
foreach (var action in repositoryActions)
|
|
||||||
{
|
|
||||||
var setupInfo = PrepareRepositoryActionAsync(executionContext, action);
|
|
||||||
if (setupInfo != null && setupInfo.Container != null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(setupInfo.Container.Image))
|
|
||||||
{
|
|
||||||
if (!state.ImagesToPull.ContainsKey(setupInfo.Container.Image))
|
|
||||||
{
|
|
||||||
state.ImagesToPull[setupInfo.Container.Image] = new List<Guid>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.Container.ActionRepository}' needs to pull image '{setupInfo.Container.Image}'");
|
|
||||||
state.ImagesToPull[setupInfo.Container.Image].Add(action.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ArgUtil.NotNullOrEmpty(setupInfo.Container.ActionRepository, nameof(setupInfo.Container.ActionRepository));
|
|
||||||
|
|
||||||
if (!state.ImagesToBuild.ContainsKey(setupInfo.Container.ActionRepository))
|
|
||||||
{
|
|
||||||
state.ImagesToBuild[setupInfo.Container.ActionRepository] = new List<Guid>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.Container.ActionRepository}' needs to build image '{setupInfo.Container.Dockerfile}'");
|
|
||||||
state.ImagesToBuild[setupInfo.Container.ActionRepository].Add(action.Id);
|
|
||||||
state.ImagesToBuildInfo[setupInfo.Container.ActionRepository] = setupInfo.Container;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (setupInfo != null && setupInfo.Steps != null && setupInfo.Steps.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var step in setupInfo.Steps)
|
|
||||||
{
|
|
||||||
nextLevel.Add((step, action.Id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve all next-level sub-actions in one batch API call,
|
|
||||||
// then recurse per parent (which hits the cache, not the API).
|
|
||||||
if (nextLevel.Count > 0)
|
|
||||||
{
|
|
||||||
var nextLevelRepoActions = nextLevel
|
|
||||||
.Where(x => x.action.Reference.Type == Pipelines.ActionSourceType.Repository)
|
|
||||||
.Select(x => x.action)
|
|
||||||
.ToList();
|
|
||||||
await ResolveNewActionsAsync(executionContext, nextLevelRepoActions, resolvedDownloadInfos);
|
|
||||||
|
|
||||||
foreach (var group in nextLevel.GroupBy(x => x.parentId))
|
|
||||||
{
|
|
||||||
var groupActions = group.Select(x => x.action).ToList();
|
|
||||||
state = await PrepareActionsRecursiveAsync(executionContext, state, groupActions, resolvedDownloadInfos, depth + 1, group.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register pre/post steps after recursion so that HasPre/HasPost
|
|
||||||
// are correct (they depend on _cachedEmbeddedPreSteps/PostSteps
|
|
||||||
// being populated by the recursive calls above).
|
|
||||||
foreach (var action in repositoryActions)
|
|
||||||
{
|
|
||||||
var repoAction = action.Reference as Pipelines.RepositoryPathReference;
|
|
||||||
if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias)
|
|
||||||
{
|
|
||||||
var definition = LoadAction(executionContext, action);
|
|
||||||
if (definition.Data.Execution.HasPre)
|
|
||||||
{
|
|
||||||
Trace.Info($"Add 'pre' execution for {action.Id}");
|
|
||||||
// Root Step
|
|
||||||
if (depth < 1)
|
|
||||||
{
|
|
||||||
var actionRunner = HostContext.CreateService<IActionRunner>();
|
|
||||||
actionRunner.Action = action;
|
|
||||||
actionRunner.Stage = ActionRunStage.Pre;
|
|
||||||
actionRunner.Condition = definition.Data.Execution.InitCondition;
|
|
||||||
state.PreStepTracker[action.Id] = actionRunner;
|
|
||||||
}
|
|
||||||
// Embedded Step
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!_cachedEmbeddedPreSteps.ContainsKey(parentStepId))
|
|
||||||
{
|
|
||||||
_cachedEmbeddedPreSteps[parentStepId] = new List<Pipelines.ActionStep>();
|
|
||||||
}
|
|
||||||
// Clone action so we can modify the condition without affecting the original
|
|
||||||
var clonedAction = action.Clone() as Pipelines.ActionStep;
|
|
||||||
clonedAction.Condition = definition.Data.Execution.InitCondition;
|
|
||||||
_cachedEmbeddedPreSteps[parentStepId].Add(clonedAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (definition.Data.Execution.HasPost && depth > 0)
|
|
||||||
{
|
|
||||||
if (!_cachedEmbeddedPostSteps.ContainsKey(parentStepId))
|
|
||||||
{
|
|
||||||
// If we haven't done so already, add the parent to the post steps
|
|
||||||
_cachedEmbeddedPostSteps[parentStepId] = new Stack<Pipelines.ActionStep>();
|
|
||||||
}
|
|
||||||
// Clone action so we can modify the condition without affecting the original
|
|
||||||
var clonedAction = action.Clone() as Pipelines.ActionStep;
|
|
||||||
clonedAction.Condition = definition.Data.Execution.CleanupCondition;
|
|
||||||
_cachedEmbeddedPostSteps[parentStepId].Push(clonedAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (depth > 0)
|
|
||||||
{
|
|
||||||
// if we're in a composite action and haven't loaded the local action yet
|
|
||||||
// we assume it has a post step
|
|
||||||
if (!_cachedEmbeddedPostSteps.ContainsKey(parentStepId))
|
|
||||||
{
|
|
||||||
// If we haven't done so already, add the parent to the post steps
|
|
||||||
_cachedEmbeddedPostSteps[parentStepId] = new Stack<Pipelines.ActionStep>();
|
|
||||||
}
|
|
||||||
// Clone action so we can modify the condition without affecting the original
|
|
||||||
var clonedAction = action.Clone() as Pipelines.ActionStep;
|
|
||||||
_cachedEmbeddedPostSteps[parentStepId].Push(clonedAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Legacy (non-batched) action resolution. Each composite resolves its
|
|
||||||
/// sub-actions individually, with no cross-depth deduplication.
|
|
||||||
/// Used when the BatchActionResolution feature flag is disabled.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<PrepareActionsState> PrepareActionsRecursiveLegacyAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Int32 depth = 0, Guid parentStepId = default(Guid))
|
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||||
if (depth > Constants.CompositeActionsMaxDepth)
|
if (depth > Constants.CompositeActionsMaxDepth)
|
||||||
@@ -449,7 +247,7 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
else if (setupInfo != null && setupInfo.Steps != null && setupInfo.Steps.Count > 0)
|
else if (setupInfo != null && setupInfo.Steps != null && setupInfo.Steps.Count > 0)
|
||||||
{
|
{
|
||||||
state = await PrepareActionsRecursiveLegacyAsync(executionContext, state, setupInfo.Steps, depth + 1, action.Id);
|
state = await PrepareActionsRecursiveAsync(executionContext, state, setupInfo.Steps, depth + 1, action.Id);
|
||||||
}
|
}
|
||||||
var repoAction = action.Reference as Pipelines.RepositoryPathReference;
|
var repoAction = action.Reference as Pipelines.RepositoryPathReference;
|
||||||
if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias)
|
if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias)
|
||||||
@@ -880,11 +678,6 @@ namespace GitHub.Runner.Worker
|
|||||||
return new Dictionary<string, WebApi.ActionDownloadInfo>();
|
return new Dictionary<string, WebApi.ActionDownloadInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass lockfile dependencies to Launch when present, so it can
|
|
||||||
// perform ref-scoped policy matching with the original refs.
|
|
||||||
var deps = executionContext.Global.ActionsDependencies;
|
|
||||||
IList<string> dependencies = (deps != null && deps.Count > 0) ? deps : null;
|
|
||||||
|
|
||||||
// Resolve download info
|
// Resolve download info
|
||||||
var launchServer = HostContext.GetService<ILaunchServer>();
|
var launchServer = HostContext.GetService<ILaunchServer>();
|
||||||
var jobServer = HostContext.GetService<IJobServer>();
|
var jobServer = HostContext.GetService<IJobServer>();
|
||||||
@@ -896,7 +689,7 @@ namespace GitHub.Runner.Worker
|
|||||||
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
|
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
|
||||||
{
|
{
|
||||||
var displayHelpfulActionsDownloadErrors = executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.DisplayHelpfulActionsDownloadErrors) ?? false;
|
var displayHelpfulActionsDownloadErrors = executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.DisplayHelpfulActionsDownloadErrors) ?? false;
|
||||||
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences, Dependencies = dependencies }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors);
|
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -961,33 +754,6 @@ namespace GitHub.Runner.Worker
|
|||||||
return actionDownloadInfos.Actions;
|
return actionDownloadInfos.Actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Only resolves actions not already in resolvedDownloadInfos.
|
|
||||||
/// Results are cached for reuse at deeper recursion levels.
|
|
||||||
/// </summary>
|
|
||||||
private async Task ResolveNewActionsAsync(IExecutionContext executionContext, List<Pipelines.ActionStep> actions, Dictionary<string, WebApi.ActionDownloadInfo> resolvedDownloadInfos)
|
|
||||||
{
|
|
||||||
var actionsToResolve = new List<Pipelines.ActionStep>();
|
|
||||||
var pendingKeys = new HashSet<string>(StringComparer.Ordinal);
|
|
||||||
foreach (var action in actions)
|
|
||||||
{
|
|
||||||
var lookupKey = GetDownloadInfoLookupKey(action);
|
|
||||||
if (!string.IsNullOrEmpty(lookupKey) && !resolvedDownloadInfos.ContainsKey(lookupKey) && pendingKeys.Add(lookupKey))
|
|
||||||
{
|
|
||||||
actionsToResolve.Add(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actionsToResolve.Count > 0)
|
|
||||||
{
|
|
||||||
var downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve);
|
|
||||||
foreach (var kvp in downloadInfos)
|
|
||||||
{
|
|
||||||
resolvedDownloadInfos[kvp.Key] = kvp.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, WebApi.ActionDownloadInfo downloadInfo)
|
private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, WebApi.ActionDownloadInfo downloadInfo)
|
||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
@@ -1007,6 +773,10 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// make sure we get a clean folder ready to use.
|
||||||
|
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
|
||||||
|
Directory.CreateDirectory(destDirectory);
|
||||||
|
|
||||||
if (downloadInfo.PackageDetails != null)
|
if (downloadInfo.PackageDetails != null)
|
||||||
{
|
{
|
||||||
executionContext.Output($"##[group]Download immutable action package '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'");
|
executionContext.Output($"##[group]Download immutable action package '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'");
|
||||||
@@ -1041,50 +811,6 @@ namespace GitHub.Runner.Worker
|
|||||||
if (!string.IsNullOrEmpty(actionArchiveCacheDir) &&
|
if (!string.IsNullOrEmpty(actionArchiveCacheDir) &&
|
||||||
Directory.Exists(actionArchiveCacheDir))
|
Directory.Exists(actionArchiveCacheDir))
|
||||||
{
|
{
|
||||||
var symlinkCachedActions = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Agent.SymlinkCachedActions));
|
|
||||||
if (symlinkCachedActions)
|
|
||||||
{
|
|
||||||
Trace.Info($"Checking if can symlink '{downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha}'");
|
|
||||||
|
|
||||||
var cacheDirectory = Path.Combine(actionArchiveCacheDir, downloadInfo.ResolvedNameWithOwner.Replace(Path.DirectorySeparatorChar, '_').Replace(Path.AltDirectorySeparatorChar, '_'), downloadInfo.ResolvedSha);
|
|
||||||
if (Directory.Exists(cacheDirectory))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Trace.Info($"Found unpacked action directory '{cacheDirectory}' in cache directory '{actionArchiveCacheDir}'");
|
|
||||||
|
|
||||||
// repository archive from github always contains a nested folder
|
|
||||||
var nestedDirectories = new DirectoryInfo(cacheDirectory).GetDirectories();
|
|
||||||
if (nestedDirectories.Length != 1)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"'{cacheDirectory}' contains '{nestedDirectories.Length}' directories");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
executionContext.Debug($"Symlink '{nestedDirectories[0].Name}' to '{destDirectory}'");
|
|
||||||
// make sure we get a clean folder ready to use.
|
|
||||||
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
|
|
||||||
IOUtil.CreateSymbolicLink(destDirectory, nestedDirectories[0].FullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
executionContext.Debug($"Created symlink from cached directory '{cacheDirectory}' to '{destDirectory}'");
|
|
||||||
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
|
|
||||||
{
|
|
||||||
Type = JobTelemetryType.General,
|
|
||||||
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache} via symlink"
|
|
||||||
});
|
|
||||||
|
|
||||||
Trace.Info("Finished getting action repository.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Error($"Failed to create symlink from cached directory '{cacheDirectory}' to '{destDirectory}'. Error: {ex}");
|
|
||||||
// Fall through to normal download logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasActionArchiveCache = true;
|
hasActionArchiveCache = true;
|
||||||
Trace.Info($"Check if action archive '{downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha}' already exists in cache directory '{actionArchiveCacheDir}'");
|
Trace.Info($"Check if action archive '{downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha}' already exists in cache directory '{actionArchiveCacheDir}'");
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
@@ -1166,10 +892,6 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// make sure we get a clean folder ready to use.
|
|
||||||
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
|
|
||||||
Directory.CreateDirectory(destDirectory);
|
|
||||||
|
|
||||||
// repository archive from github always contains a nested folder
|
// repository archive from github always contains a nested folder
|
||||||
var subDirectories = new DirectoryInfo(stagingDirectory).GetDirectories();
|
var subDirectories = new DirectoryInfo(stagingDirectory).GetDirectories();
|
||||||
if (subDirectories.Length != 1)
|
if (subDirectories.Length != 1)
|
||||||
@@ -1372,29 +1094,16 @@ namespace GitHub.Runner.Worker
|
|||||||
return $"{repositoryReference.Name}@{repositoryReference.Ref}";
|
return $"{repositoryReference.Name}@{repositoryReference.Ref}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthenticationHeaderValue CreateAuthHeader(IExecutionContext executionContext, string downloadUrl, string token)
|
private AuthenticationHeaderValue CreateAuthHeader(string token)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.UseBearerTokenForCodeload) == true &&
|
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{token}"));
|
||||||
Uri.TryCreate(downloadUrl, UriKind.Absolute, out var parsedUrl) &&
|
HostContext.SecretMasker.AddValue(base64EncodingToken);
|
||||||
!string.IsNullOrEmpty(parsedUrl?.Host) &&
|
return new AuthenticationHeaderValue("Basic", base64EncodingToken);
|
||||||
!string.IsNullOrEmpty(parsedUrl?.PathAndQuery) &&
|
|
||||||
(parsedUrl.Host.StartsWith("codeload.", StringComparison.OrdinalIgnoreCase) || parsedUrl.PathAndQuery.StartsWith("/_codeload/", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
Trace.Info("Using Bearer token for action archive download directly to codeload.");
|
|
||||||
return new AuthenticationHeaderValue("Bearer", token);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Trace.Info("Using Basic token for action archive download.");
|
|
||||||
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{token}"));
|
|
||||||
HostContext.SecretMasker.AddValue(base64EncodingToken);
|
|
||||||
return new AuthenticationHeaderValue("Basic", base64EncodingToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DownloadRepositoryArchive(IExecutionContext executionContext, string downloadUrl, string downloadAuthToken, string archiveFile)
|
private async Task DownloadRepositoryArchive(IExecutionContext executionContext, string downloadUrl, string downloadAuthToken, string archiveFile)
|
||||||
@@ -1404,111 +1113,92 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
// Allow up to 20 * 60s for any action to be downloaded from github graph.
|
// Allow up to 20 * 60s for any action to be downloaded from github graph.
|
||||||
int timeoutSeconds = 20 * 60;
|
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;
|
try
|
||||||
using (var actionDownloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
|
|
||||||
using (var actionDownloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(actionDownloadTimeout.Token, executionContext.CancellationToken))
|
|
||||||
{
|
{
|
||||||
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
|
httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadAuthToken);
|
||||||
using (FileStream fs = new(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: _defaultFileStreamBufferSize, useAsync: true))
|
|
||||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||||
using (var httpClient = new HttpClient(httpClientHandler))
|
using (var response = await httpClient.GetAsync(downloadUrl))
|
||||||
{
|
{
|
||||||
httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(executionContext, downloadUrl, downloadAuthToken);
|
requestId = UrlUtil.GetGitHubRequestId(response.Headers);
|
||||||
|
if (!string.IsNullOrEmpty(requestId))
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
|
||||||
using (var response = await httpClient.GetAsync(downloadUrl))
|
|
||||||
{
|
{
|
||||||
requestId = UrlUtil.GetGitHubRequestId(response.Headers);
|
Trace.Info($"Request URL: {downloadUrl} X-GitHub-Request-Id: {requestId} Http Status: {response.StatusCode}");
|
||||||
if (!string.IsNullOrEmpty(requestId))
|
}
|
||||||
{
|
|
||||||
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.
|
// download succeed, break out the retry loop.
|
||||||
break;
|
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 if (response.StatusCode == HttpStatusCode.Forbidden)
|
|
||||||
{
|
|
||||||
// It doesn't make sense to retry in this case, so just stop
|
|
||||||
throw new AccessDeniedException($"Access denied to '{downloadUrl}' ({requestId})");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Something else bad happened, let's go to our retry logic
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 (AccessDeniedException)
|
|
||||||
{
|
|
||||||
Trace.Info($"Access denied to '{downloadUrl}'");
|
|
||||||
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 (OperationCanceledException) when (executionContext.CancellationToken.IsCancellationRequested)
|
||||||
if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF")))
|
|
||||||
{
|
{
|
||||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
Trace.Info("Action download has been cancelled.");
|
||||||
executionContext.Warning($"Back off {backOff.TotalSeconds} seconds before retry.");
|
throw;
|
||||||
await Task.Delay(backOff);
|
}
|
||||||
|
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 AccessDeniedException) && !(ex is OperationCanceledException) && !executionContext.CancellationToken.IsCancellationRequested)
|
if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF")))
|
||||||
{
|
{
|
||||||
Trace.Error($"Failed to download archive '{downloadUrl}' after {retryCount + 1} attempts.");
|
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
||||||
Trace.Error(ex);
|
executionContext.Warning($"Back off {backOff.TotalSeconds} seconds before retry.");
|
||||||
throw new FailedToDownloadActionException($"Failed to download archive '{downloadUrl}' after {retryCount + 1} attempts.", ex);
|
await Task.Delay(backOff);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ArgUtil.NotNullOrEmpty(archiveFile, nameof(archiveFile));
|
ArgUtil.NotNullOrEmpty(archiveFile, nameof(archiveFile));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ namespace GitHub.Runner.Worker
|
|||||||
"EvaluateContainerEnvironment",
|
"EvaluateContainerEnvironment",
|
||||||
() => _legacyManager.EvaluateContainerEnvironment(executionContext, token, extraExpressionValues),
|
() => _legacyManager.EvaluateContainerEnvironment(executionContext, token, extraExpressionValues),
|
||||||
() => _newManager.EvaluateContainerEnvironment(executionContext, ConvertToNewToken(token) as GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.MappingToken, ConvertToNewExpressionValues(extraExpressionValues)),
|
() => _newManager.EvaluateContainerEnvironment(executionContext, ConvertToNewToken(token) as GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.MappingToken, ConvertToNewExpressionValues(extraExpressionValues)),
|
||||||
(legacyResult, newResult) =>
|
(legacyResult, newResult) => {
|
||||||
{
|
|
||||||
var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper));
|
var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper));
|
||||||
return CompareDictionaries(trace, legacyResult, newResult, "ContainerEnvironment");
|
return CompareDictionaries(trace, legacyResult, newResult, "ContainerEnvironment");
|
||||||
});
|
});
|
||||||
@@ -166,150 +165,9 @@ namespace GitHub.Runner.Worker
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = new List<GitHub.DistributedTask.Pipelines.ActionStep>();
|
// Serialize new steps and deserialize to old steps
|
||||||
foreach (var step in newSteps)
|
var json = StringUtil.ConvertToJson(newSteps, Newtonsoft.Json.Formatting.None);
|
||||||
{
|
return StringUtil.ConvertFromJson<List<GitHub.DistributedTask.Pipelines.ActionStep>>(json);
|
||||||
var actionStep = new GitHub.DistributedTask.Pipelines.ActionStep
|
|
||||||
{
|
|
||||||
ContextName = step.Id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (step is GitHub.Actions.WorkflowParser.RunStep runStep)
|
|
||||||
{
|
|
||||||
actionStep.Condition = ExtractConditionString(runStep.If);
|
|
||||||
actionStep.DisplayNameToken = ConvertToLegacyToken<TemplateToken>(runStep.Name);
|
|
||||||
actionStep.ContinueOnError = ConvertToLegacyToken<TemplateToken>(runStep.ContinueOnError);
|
|
||||||
actionStep.TimeoutInMinutes = ConvertToLegacyToken<TemplateToken>(runStep.TimeoutMinutes);
|
|
||||||
actionStep.Environment = ConvertToLegacyToken<TemplateToken>(runStep.Env);
|
|
||||||
actionStep.Reference = new GitHub.DistributedTask.Pipelines.ScriptReference();
|
|
||||||
actionStep.Inputs = BuildRunStepInputs(runStep);
|
|
||||||
}
|
|
||||||
else if (step is GitHub.Actions.WorkflowParser.ActionStep usesStep)
|
|
||||||
{
|
|
||||||
actionStep.Condition = ExtractConditionString(usesStep.If);
|
|
||||||
actionStep.DisplayNameToken = ConvertToLegacyToken<TemplateToken>(usesStep.Name);
|
|
||||||
actionStep.ContinueOnError = ConvertToLegacyToken<TemplateToken>(usesStep.ContinueOnError);
|
|
||||||
actionStep.TimeoutInMinutes = ConvertToLegacyToken<TemplateToken>(usesStep.TimeoutMinutes);
|
|
||||||
actionStep.Environment = ConvertToLegacyToken<TemplateToken>(usesStep.Env);
|
|
||||||
actionStep.Reference = ParseActionReference(usesStep.Uses?.Value);
|
|
||||||
actionStep.Inputs = ConvertToLegacyToken<MappingToken>(usesStep.With);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(actionStep);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ExtractConditionString(GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.BasicExpressionToken ifToken)
|
|
||||||
{
|
|
||||||
if (ifToken == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Expression property is internal, so we use ToString() which formats as "${{ expr }}"
|
|
||||||
// Then strip the delimiters to get just the expression
|
|
||||||
var str = ifToken.ToString();
|
|
||||||
if (str.StartsWith("${{") && str.EndsWith("}}"))
|
|
||||||
{
|
|
||||||
return str.Substring(3, str.Length - 5).Trim();
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MappingToken BuildRunStepInputs(GitHub.Actions.WorkflowParser.RunStep runStep)
|
|
||||||
{
|
|
||||||
var inputs = new MappingToken(null, null, null);
|
|
||||||
|
|
||||||
// script (from run)
|
|
||||||
if (runStep.Run != null)
|
|
||||||
{
|
|
||||||
inputs.Add(
|
|
||||||
new StringToken(null, null, null, "script"),
|
|
||||||
ConvertToLegacyToken<TemplateToken>(runStep.Run));
|
|
||||||
}
|
|
||||||
|
|
||||||
// shell
|
|
||||||
if (runStep.Shell != null)
|
|
||||||
{
|
|
||||||
inputs.Add(
|
|
||||||
new StringToken(null, null, null, "shell"),
|
|
||||||
ConvertToLegacyToken<TemplateToken>(runStep.Shell));
|
|
||||||
}
|
|
||||||
|
|
||||||
// working-directory
|
|
||||||
if (runStep.WorkingDirectory != null)
|
|
||||||
{
|
|
||||||
inputs.Add(
|
|
||||||
new StringToken(null, null, null, "workingDirectory"),
|
|
||||||
ConvertToLegacyToken<TemplateToken>(runStep.WorkingDirectory));
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputs.Count > 0 ? inputs : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GitHub.DistributedTask.Pipelines.ActionStepDefinitionReference ParseActionReference(string uses)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(uses))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Docker reference: docker://image:tag
|
|
||||||
if (uses.StartsWith("docker://", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return new GitHub.DistributedTask.Pipelines.ContainerRegistryReference
|
|
||||||
{
|
|
||||||
Image = uses.Substring("docker://".Length)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local path reference: ./path/to/action
|
|
||||||
if (uses.StartsWith("./") || uses.StartsWith(".\\"))
|
|
||||||
{
|
|
||||||
return new GitHub.DistributedTask.Pipelines.RepositoryPathReference
|
|
||||||
{
|
|
||||||
RepositoryType = "self",
|
|
||||||
Path = uses
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repository reference: owner/repo@ref or owner/repo/path@ref
|
|
||||||
var atIndex = uses.LastIndexOf('@');
|
|
||||||
string refPart = null;
|
|
||||||
string repoPart = uses;
|
|
||||||
|
|
||||||
if (atIndex > 0)
|
|
||||||
{
|
|
||||||
refPart = uses.Substring(atIndex + 1);
|
|
||||||
repoPart = uses.Substring(0, atIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split by / to get owner/repo and optional path
|
|
||||||
var parts = repoPart.Split('/');
|
|
||||||
string name;
|
|
||||||
string path = null;
|
|
||||||
|
|
||||||
if (parts.Length >= 2)
|
|
||||||
{
|
|
||||||
name = $"{parts[0]}/{parts[1]}";
|
|
||||||
if (parts.Length > 2)
|
|
||||||
{
|
|
||||||
path = string.Join("/", parts, 2, parts.Length - 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
name = repoPart;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new GitHub.DistributedTask.Pipelines.RepositoryPathReference
|
|
||||||
{
|
|
||||||
RepositoryType = "GitHub",
|
|
||||||
Name = name,
|
|
||||||
Ref = refPart,
|
|
||||||
Path = path
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private T ConvertToLegacyToken<T>(GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken newToken) where T : TemplateToken
|
private T ConvertToLegacyToken<T>(GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken newToken) where T : TemplateToken
|
||||||
@@ -775,14 +633,6 @@ namespace GitHub.Runner.Worker
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for known equivalent error patterns (e.g., JSON parse errors)
|
|
||||||
// where both parsers correctly reject invalid input but with different wording
|
|
||||||
if (PipelineTemplateEvaluatorWrapper.HasJsonExceptionType(legacyException) && PipelineTemplateEvaluatorWrapper.HasJsonExceptionType(newException))
|
|
||||||
{
|
|
||||||
trace.Info("CompareExceptions - both exceptions are JSON parse errors, treating as matched");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare exception messages recursively (including inner exceptions)
|
// Compare exception messages recursively (including inner exceptions)
|
||||||
var legacyMessages = GetExceptionMessages(legacyException);
|
var legacyMessages = GetExceptionMessages(legacyException);
|
||||||
var newMessages = GetExceptionMessages(newException);
|
var newMessages = GetExceptionMessages(newException);
|
||||||
@@ -847,6 +697,5 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,14 +379,7 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
prefix = PipelineTemplateConstants.RunDisplayPrefix;
|
prefix = PipelineTemplateConstants.RunDisplayPrefix;
|
||||||
var repositoryReference = action.Reference as RepositoryPathReference;
|
var repositoryReference = action.Reference as RepositoryPathReference;
|
||||||
var pathString = string.Empty;
|
var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
|
||||||
if (!string.IsNullOrEmpty(repositoryReference.Path))
|
|
||||||
{
|
|
||||||
// For local actions (Name is empty), don't prepend "/" to avoid "/./"
|
|
||||||
pathString = string.IsNullOrEmpty(repositoryReference.Name)
|
|
||||||
? repositoryReference.Path
|
|
||||||
: $"/{repositoryReference.Path}";
|
|
||||||
}
|
|
||||||
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
|
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
|
||||||
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
|
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
|
||||||
tokenToParse = new StringToken(null, null, null, repoString);
|
tokenToParse = new StringToken(null, null, null, repoString);
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Pure data for control-flow steps (wait, wait-all, cancel).
|
|
||||||
/// Type uses Pipelines.BackgroundControlTypes string constants.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BackgroundStepControlFlowData
|
|
||||||
{
|
|
||||||
public string Type { get; set; }
|
|
||||||
public Guid StepId { get; set; }
|
|
||||||
public string StepName { get; set; }
|
|
||||||
|
|
||||||
// Target step IDs (for wait: steps to wait for; for cancel: steps to cancel)
|
|
||||||
public string[] StepIds { get; set; }
|
|
||||||
|
|
||||||
// Parallel group ID for grouping steps in the UI
|
|
||||||
public string ParallelGroupId { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
|
||||||
{
|
|
||||||
[ServiceLocator(Default = typeof(BackgroundStepCoordinator))]
|
|
||||||
public interface IBackgroundStepCoordinator : IRunnerService
|
|
||||||
{
|
|
||||||
void InitializeCoordinator(int maxConcurrent);
|
|
||||||
void StartBackgroundStep(IStep step, CancellationToken jobCancellationToken);
|
|
||||||
Task<TaskResult> WaitForUnwaitedStepsAsync(CancellationToken cancellationToken);
|
|
||||||
Task RunControlFlowAsync(IExecutionContext stepContext, object data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Coordinates background step execution, waiting, cancellation, and deferred state.
|
|
||||||
/// Extracted from StepsRunner so the main step loop stays clean.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BackgroundStepCoordinator : RunnerService, IBackgroundStepCoordinator
|
|
||||||
{
|
|
||||||
private const int DefaultMaxBackgroundSteps = 10;
|
|
||||||
private readonly Dictionary<string, (IStep Step, Task Task, CancellationTokenSource Cts)> _backgroundSteps = new();
|
|
||||||
|
|
||||||
// IDs of background steps that have already been completed (waited on or canceled).
|
|
||||||
// Used to avoid waiting on or flushing the same step more than once.
|
|
||||||
private readonly HashSet<string> _completedStepIds = new();
|
|
||||||
|
|
||||||
// IDs of background steps that were explicitly canceled via a `cancel` control step.
|
|
||||||
// These steps are expected to be canceled, so their (Canceled) result must not be
|
|
||||||
// merged into the overall job result.
|
|
||||||
private readonly HashSet<string> _explicitlyCanceledStepIds = new();
|
|
||||||
private SemaphoreSlim _backgroundSlotSemaphore = new SemaphoreSlim(DefaultMaxBackgroundSteps);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reset per-job state. Call at the start of each job.
|
|
||||||
/// </summary>
|
|
||||||
public void InitializeCoordinator(int maxConcurrent)
|
|
||||||
{
|
|
||||||
_backgroundSteps.Clear();
|
|
||||||
_completedStepIds.Clear();
|
|
||||||
_explicitlyCanceledStepIds.Clear();
|
|
||||||
var max = maxConcurrent > 0 ? maxConcurrent : DefaultMaxBackgroundSteps;
|
|
||||||
_backgroundSlotSemaphore = new SemaphoreSlim(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Starting background steps
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prepare and launch a background step. Does not block the caller.
|
|
||||||
/// </summary>
|
|
||||||
public void StartBackgroundStep(IStep step, CancellationToken jobCancellationToken)
|
|
||||||
{
|
|
||||||
var stepId = step.ExecutionContext?.ContextName ?? step.DisplayName;
|
|
||||||
|
|
||||||
// Isolate GitHubContext so concurrent steps don't overwrite each other's GITHUB_OUTPUT paths
|
|
||||||
if (step.ExecutionContext.ExpressionValues.TryGetValue("github", out var ghCtx) && ghCtx is GitHubContext sharedGitHub)
|
|
||||||
{
|
|
||||||
step.ExecutionContext.ExpressionValues["github"] = sharedGitHub.ShallowCopy();
|
|
||||||
}
|
|
||||||
|
|
||||||
var bgCts = CancellationTokenSource.CreateLinkedTokenSource(jobCancellationToken);
|
|
||||||
|
|
||||||
// Evaluate timeout on the main thread (needs expression context)
|
|
||||||
var timeoutMinutes = 0;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator();
|
|
||||||
timeoutMinutes = templateEvaluator.EvaluateStepTimeout(step.Timeout, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Info($"Error determining timeout for background step '{stepId}': {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var task = ExecuteBackgroundStepCoreAsync(step, bgCts, stepId, timeoutMinutes);
|
|
||||||
_backgroundSteps[stepId] = (step, task, bgCts);
|
|
||||||
Trace.Info($"Background step '{stepId}' queued (slot will be acquired asynchronously).");
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Safety net
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
|
|
||||||
// Drain any background steps that weren't already waited on by an explicit wait/cancel
|
|
||||||
// control step, then merge the final results of all background steps into a single result
|
|
||||||
// for the caller to fold into the job result.
|
|
||||||
public async Task<TaskResult> WaitForUnwaitedStepsAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var unwaitedIds = _backgroundSteps.Keys.Where(id => !_completedStepIds.Contains(id)).ToList();
|
|
||||||
if (unwaitedIds.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Info($"Safety net: {unwaitedIds.Count} unwaited background step(s) at post-job boundary: {string.Join(", ", unwaitedIds)}");
|
|
||||||
await WaitForStepTasksAsync(unwaitedIds, cancellationToken);
|
|
||||||
CompleteWaitedSteps(unwaitedIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = TaskResult.Succeeded;
|
|
||||||
foreach (var (stepId, (step, _, _)) in _backgroundSteps)
|
|
||||||
{
|
|
||||||
// A step that succeeded does not set a Result by default, so a missing
|
|
||||||
// value means the step succeeded and there is nothing to merge.
|
|
||||||
if (!step.ExecutionContext.Result.HasValue)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A step explicitly canceled via a `cancel` control step is expected to be canceled,
|
|
||||||
// so a Canceled result must not influence the overall job result. However, if the step
|
|
||||||
// failed (e.g. before the cancellation took effect), that failure should still count.
|
|
||||||
if (_explicitlyCanceledStepIds.Contains(stepId) &&
|
|
||||||
step.ExecutionContext.Result.Value == TaskResult.Canceled)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = TaskResultUtil.MergeTaskResults(result, step.ExecutionContext.Result.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result != TaskResult.Succeeded)
|
|
||||||
{
|
|
||||||
Trace.Info($"Background steps reported result '{result}' to caller.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Control-flow step dispatch
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a control-flow step (wait, wait-all, cancel) and propagate results.
|
|
||||||
/// </summary>
|
|
||||||
public async Task RunControlFlowAsync(IExecutionContext stepContext, object data)
|
|
||||||
{
|
|
||||||
var controlFlow = data as BackgroundStepControlFlowData;
|
|
||||||
switch (controlFlow.Type)
|
|
||||||
{
|
|
||||||
case Pipelines.BackgroundControlTypes.Wait:
|
|
||||||
{
|
|
||||||
var ids = controlFlow.StepIds ?? Array.Empty<string>();
|
|
||||||
stepContext.Output($"Waiting for background step(s) to complete: {DescribeSteps(ids)}");
|
|
||||||
await WaitForStepTasksAsync(ids, stepContext.CancellationToken);
|
|
||||||
stepContext.Result = CompleteWaitedSteps(ids);
|
|
||||||
ReportCompletedSteps(stepContext, "Finished waiting for background step(s).", ids);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case Pipelines.BackgroundControlTypes.WaitAll:
|
|
||||||
{
|
|
||||||
var remaining = _backgroundSteps.Keys.Where(id => !_completedStepIds.Contains(id)).ToList();
|
|
||||||
stepContext.Output(remaining.Count > 0
|
|
||||||
? $"Waiting for all background step(s) to complete: {DescribeSteps(remaining)}"
|
|
||||||
: "No background steps remaining to wait for.");
|
|
||||||
await WaitForStepTasksAsync(remaining, stepContext.CancellationToken);
|
|
||||||
stepContext.Result = CompleteWaitedSteps(remaining);
|
|
||||||
ReportCompletedSteps(stepContext, "Finished waiting for all background step(s).", remaining);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case Pipelines.BackgroundControlTypes.Cancel:
|
|
||||||
{
|
|
||||||
var cancelIds = controlFlow.StepIds ?? Array.Empty<string>();
|
|
||||||
stepContext.Output($"Cancelling background step(s): {DescribeSteps(cancelIds)}");
|
|
||||||
await CancelStepsAsync(controlFlow.StepIds);
|
|
||||||
stepContext.Result = TaskResult.Succeeded;
|
|
||||||
ReportCompletedSteps(stepContext, "Finished cancelling background step(s).", cancelIds);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new ArgumentException($"Unknown background step control type '{controlFlow.Type}'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Private helpers
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
|
|
||||||
// Resolve background step IDs to their display names for customer-facing output.
|
|
||||||
private string DescribeSteps(IEnumerable<string> stepIds)
|
|
||||||
{
|
|
||||||
var names = stepIds
|
|
||||||
.Select(id => _backgroundSteps.TryGetValue(id, out var entry) ? entry.Step.DisplayName : id)
|
|
||||||
.ToList();
|
|
||||||
return names.Count > 0 ? string.Join(", ", names) : "(none)";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit a completion summary plus the final result of each affected background step.
|
|
||||||
private void ReportCompletedSteps(IExecutionContext stepContext, string summary, IEnumerable<string> stepIds)
|
|
||||||
{
|
|
||||||
stepContext.Output(summary);
|
|
||||||
foreach (var id in stepIds)
|
|
||||||
{
|
|
||||||
if (_backgroundSteps.TryGetValue(id, out var entry))
|
|
||||||
{
|
|
||||||
var result = entry.Step.ExecutionContext.Result?.ToString() ?? "Unknown";
|
|
||||||
stepContext.Output($" {entry.Step.DisplayName}: {result}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExecuteBackgroundStepCoreAsync(
|
|
||||||
IStep step, CancellationTokenSource bgCts,
|
|
||||||
string stepId, int timeoutMinutes)
|
|
||||||
{
|
|
||||||
Trace.Info($"Background step '{stepId}' waiting for slot.");
|
|
||||||
await _backgroundSlotSemaphore.WaitAsync(bgCts.Token);
|
|
||||||
Trace.Info($"Background step '{stepId}' acquired slot.");
|
|
||||||
|
|
||||||
step.ExecutionContext.Start();
|
|
||||||
|
|
||||||
if (timeoutMinutes > 0)
|
|
||||||
{
|
|
||||||
step.ExecutionContext.SetTimeout(TimeSpan.FromMinutes(timeoutMinutes));
|
|
||||||
}
|
|
||||||
|
|
||||||
using var cancelReg = bgCts.Token.Register(() =>
|
|
||||||
{
|
|
||||||
Trace.Info($"Background step '{stepId}': cancellation signalled, sending CancelToken to process.");
|
|
||||||
step.ExecutionContext.CancelToken();
|
|
||||||
});
|
|
||||||
|
|
||||||
TaskResult? result = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await step.RunAsync();
|
|
||||||
result = step.ExecutionContext.Result ?? TaskResult.Succeeded;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (bgCts.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
result = TaskResult.Canceled;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (step.ExecutionContext.CancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
Trace.Info($"Background step '{stepId}' timed out after {timeoutMinutes} minutes.");
|
|
||||||
step.ExecutionContext.Error($"The background step '{step.DisplayName}' has timed out after {timeoutMinutes} minutes.");
|
|
||||||
result = TaskResult.Failed;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Info($"Background step '{stepId}' failed: {ex.Message}");
|
|
||||||
step.ExecutionContext.Error(ex);
|
|
||||||
result = TaskResult.Failed;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_backgroundSlotSemaphore.Release();
|
|
||||||
|
|
||||||
if (step.ExecutionContext.CommandResult != null)
|
|
||||||
{
|
|
||||||
result = TaskResultUtil.MergeTaskResults(result, step.ExecutionContext.CommandResult.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
step.ExecutionContext.Result = result;
|
|
||||||
step.ExecutionContext.ApplyContinueOnError(step.ContinueOnError);
|
|
||||||
|
|
||||||
step.ExecutionContext.Complete(step.ExecutionContext.Result);
|
|
||||||
Trace.Info($"Background step '{stepId}' completed with result: {step.ExecutionContext.Result}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CancelStepsAsync(string[] cancelStepIds)
|
|
||||||
{
|
|
||||||
if (cancelStepIds == null || cancelStepIds.Length == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark these steps as expected-to-be-canceled so their result does not
|
|
||||||
// affect the overall job result.
|
|
||||||
foreach (var id in cancelStepIds)
|
|
||||||
{
|
|
||||||
_explicitlyCanceledStepIds.Add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
var idsToCancel = cancelStepIds
|
|
||||||
.Where(id => _backgroundSteps.ContainsKey(id) && !_backgroundSteps[id].Task.IsCompleted)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
if (idsToCancel.Length > 0)
|
|
||||||
{
|
|
||||||
Trace.Info($"Cancelling {idsToCancel.Length} background step(s): {string.Join(", ", idsToCancel)}");
|
|
||||||
await CancelWithGracePeriodAsync(idsToCancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush deferred state and mark canceled steps as completed.
|
|
||||||
CompleteWaitedSteps(cancelStepIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task WaitForStepTasksAsync(IEnumerable<string> stepIds, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var ids = stepIds.ToList();
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
|
|
||||||
foreach (var stepId in ids)
|
|
||||||
{
|
|
||||||
if (_backgroundSteps.TryGetValue(stepId, out var entry) && !entry.Task.IsCompleted)
|
|
||||||
{
|
|
||||||
tasks.Add(entry.Task);
|
|
||||||
}
|
|
||||||
else if (!_backgroundSteps.ContainsKey(stepId))
|
|
||||||
{
|
|
||||||
Trace.Info($"Wait references unknown background step: {stepId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tasks.Count > 0)
|
|
||||||
{
|
|
||||||
Trace.Info($"Waiting for {tasks.Count} background step(s)...");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.WhenAll(tasks).WaitAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
Trace.Info("Wait interrupted by job cancellation — cancelling background steps.");
|
|
||||||
await CancelWithGracePeriodAsync(ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CancelWithGracePeriodAsync(IEnumerable<string> stepIds, double graceSeconds = 7.5)
|
|
||||||
{
|
|
||||||
var cancelledSteps = new List<(string StepId, Task Task, IStep Step)>();
|
|
||||||
foreach (var stepId in stepIds)
|
|
||||||
{
|
|
||||||
if (_backgroundSteps.TryGetValue(stepId, out var entry) && !entry.Task.IsCompleted)
|
|
||||||
{
|
|
||||||
entry.Step.ExecutionContext.CancelToken();
|
|
||||||
entry.Cts.Cancel();
|
|
||||||
cancelledSteps.Add((stepId, entry.Task, entry.Step));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelledSteps.Count > 0)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.WhenAll(cancelledSteps.Select(s => s.Task)).WaitAsync(TimeSpan.FromSeconds(graceSeconds));
|
|
||||||
}
|
|
||||||
catch (TimeoutException)
|
|
||||||
{
|
|
||||||
Trace.Info($"Some background steps did not terminate within {graceSeconds}s grace period.");
|
|
||||||
|
|
||||||
// The step tasks above never completed, so their finally block never ran and
|
|
||||||
// their result was never set. Force-mark them as canceled so the abandoned
|
|
||||||
// steps still report a terminal result.
|
|
||||||
foreach (var (stepId, task, step) in cancelledSteps)
|
|
||||||
{
|
|
||||||
if (!task.IsCompleted && !step.ExecutionContext.Result.HasValue)
|
|
||||||
{
|
|
||||||
step.ExecutionContext.Result = TaskResult.Canceled;
|
|
||||||
Trace.Info($"Background step '{stepId}' did not terminate within grace period; marking as canceled.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TaskResult CompleteWaitedSteps(IEnumerable<string> stepIds)
|
|
||||||
{
|
|
||||||
var result = TaskResult.Succeeded;
|
|
||||||
foreach (var id in stepIds)
|
|
||||||
{
|
|
||||||
_completedStepIds.Add(id);
|
|
||||||
if (_backgroundSteps.TryGetValue(id, out var entry))
|
|
||||||
{
|
|
||||||
// Flush deferred state for the completed step.
|
|
||||||
entry.Step.ExecutionContext.FlushDeferredOutputs();
|
|
||||||
entry.Step.ExecutionContext.FlushDeferredEnvironment();
|
|
||||||
entry.Step.ExecutionContext.FlushDeferredOutcomeConclusion();
|
|
||||||
Trace.Info($"Flushed deferred state for background step '{id}'.");
|
|
||||||
|
|
||||||
if (entry.Step.ExecutionContext.Result.HasValue)
|
|
||||||
{
|
|
||||||
result = TaskResultUtil.MergeTaskResults(result, entry.Step.ExecutionContext.Result.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,8 +36,6 @@ namespace GitHub.Runner.Worker.Container
|
|||||||
this.ContainerImage = containerImage;
|
this.ContainerImage = containerImage;
|
||||||
this.ContainerDisplayName = $"{container.Alias}_{Pipelines.Validation.NameValidation.Sanitize(containerImage)}_{Guid.NewGuid().ToString("N").Substring(0, 6)}";
|
this.ContainerDisplayName = $"{container.Alias}_{Pipelines.Validation.NameValidation.Sanitize(containerImage)}_{Guid.NewGuid().ToString("N").Substring(0, 6)}";
|
||||||
this.ContainerCreateOptions = container.Options;
|
this.ContainerCreateOptions = container.Options;
|
||||||
this.ContainerEntryPoint = container.Entrypoint;
|
|
||||||
this.ContainerEntryPointArgs = container.Command;
|
|
||||||
_environmentVariables = container.Environment;
|
_environmentVariables = container.Environment;
|
||||||
this.IsJobContainer = isJobContainer;
|
this.IsJobContainer = isJobContainer;
|
||||||
this.ContainerNetworkAlias = networkAlias;
|
this.ContainerNetworkAlias = networkAlias;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,450 +0,0 @@
|
|||||||
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.Container;
|
|
||||||
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,
|
|
||||||
bool isActionStep,
|
|
||||||
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, isActionStep, 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,
|
|
||||||
bool isActionStep,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// 1. Resolve step host — container or host, same as ActionRunner.
|
|
||||||
// Only action steps (user-defined run:/uses:) execute inside the
|
|
||||||
// container. Infrastructure steps (Set up job, Initialize
|
|
||||||
// containers, Complete job, etc.) always run on the host.
|
|
||||||
var stepHost = CreateStepHost(context, isActionStep);
|
|
||||||
var isContainerStepHost = stepHost is IContainerStepHost;
|
|
||||||
|
|
||||||
// 2. 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 (container={isContainerStepHost})");
|
|
||||||
|
|
||||||
// 3. 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
|
|
||||||
{
|
|
||||||
// 4. Resolve script path — translate for container if needed
|
|
||||||
var resolvedPath = stepHost.ResolvePathForStepHost(context, 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);
|
|
||||||
|
|
||||||
// 5. Resolve shell command path — for containers, use the shell
|
|
||||||
// name directly (it will be resolved inside the container);
|
|
||||||
// for host execution, resolve the full path on the host.
|
|
||||||
string prependPath = string.Join(
|
|
||||||
Path.PathSeparator.ToString(),
|
|
||||||
Enumerable.Reverse(context.Global.PrependPath));
|
|
||||||
var fileName = isContainerStepHost
|
|
||||||
? shellCommand
|
|
||||||
: WhichUtil.Which(shellCommand, false, _trace, prependPath) ?? shellCommand;
|
|
||||||
|
|
||||||
// 6. Build environment — merge from execution context like a real step
|
|
||||||
var environment = BuildEnvironment(context, command.Env);
|
|
||||||
|
|
||||||
// 7. Handle PrependPath — mirrors Handler.AddPrependPathToEnvironment
|
|
||||||
if (context.Global.PrependPath.Count > 0)
|
|
||||||
{
|
|
||||||
if (stepHost is IContainerStepHost containerHost)
|
|
||||||
{
|
|
||||||
containerHost.PrependPath = prependPath;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string taskEnvPATH;
|
|
||||||
environment.TryGetValue(Constants.PathVariable, out taskEnvPATH);
|
|
||||||
string originalPath = context.Global.Variables?.Get(Constants.PathVariable) ?? // Prefer a job variable.
|
|
||||||
taskEnvPATH ?? // Then a task-environment variable.
|
|
||||||
System.Environment.GetEnvironmentVariable(Constants.PathVariable) ?? // Then an environment variable.
|
|
||||||
string.Empty;
|
|
||||||
environment[Constants.PathVariable] = PathUtil.PrependPath(prependPath, originalPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Resolve working directory — translate for container
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
workingDirectory = stepHost.ResolvePathForStepHost(context, workingDirectory);
|
|
||||||
|
|
||||||
_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");
|
|
||||||
|
|
||||||
// NOTE: When container hooks are enabled, ContainerStepHost routes
|
|
||||||
// execution through IContainerHookManager which does not raise
|
|
||||||
// OutputDataReceived/ErrorDataReceived events. Output will not be
|
|
||||||
// streamed to the debug console in that mode.
|
|
||||||
if (isContainerStepHost && FeatureManager.IsContainerHooksEnabled(context.Global?.Variables))
|
|
||||||
{
|
|
||||||
const string hookWarning = "Container hooks are enabled. REPL output will not be streamed to the debug console for this command.";
|
|
||||||
_trace.Warning(hookWarning);
|
|
||||||
SendOutput("stderr", hookWarning + "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. Execute via IStepHost — handles docker exec for containers,
|
|
||||||
// direct process execution for host, and container hooks
|
|
||||||
stepHost.OutputDataReceived += (sender, args) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(args.Data))
|
|
||||||
{
|
|
||||||
var masked = _hostContext.SecretMasker.MaskSecrets(args.Data);
|
|
||||||
SendOutput("stdout", masked + "\n");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
stepHost.ErrorDataReceived += (sender, args) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(args.Data))
|
|
||||||
{
|
|
||||||
var masked = _hostContext.SecretMasker.MaskSecrets(args.Data);
|
|
||||||
SendOutput("stderr", masked + "\n");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
int exitCode = await stepHost.ExecuteAsync(
|
|
||||||
context: context,
|
|
||||||
workingDirectory: workingDirectory,
|
|
||||||
fileName: fileName,
|
|
||||||
arguments: arguments,
|
|
||||||
environment: environment,
|
|
||||||
requireExitCodeZero: false,
|
|
||||||
outputEncoding: null,
|
|
||||||
killProcessOnCancel: true,
|
|
||||||
inheritConsoleHandler: false,
|
|
||||||
standardInInput: null,
|
|
||||||
cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
_trace.Info($"REPL command exited with code {exitCode}");
|
|
||||||
|
|
||||||
// 10. 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>
|
|
||||||
/// Creates the appropriate <see cref="IStepHost"/> for the current
|
|
||||||
/// execution context, mirroring how <see cref="ActionRunner"/> decides
|
|
||||||
/// between host and container execution.
|
|
||||||
///
|
|
||||||
/// Only action steps (user-defined run:/uses: steps) run inside the
|
|
||||||
/// job container. Infrastructure steps like "Set up job", "Initialize
|
|
||||||
/// containers", "Stop containers", and "Complete job" always execute
|
|
||||||
/// on the host regardless of whether a container is configured.
|
|
||||||
/// </summary>
|
|
||||||
internal IStepHost CreateStepHost(IExecutionContext context, bool isActionStep)
|
|
||||||
{
|
|
||||||
if (!isActionStep)
|
|
||||||
{
|
|
||||||
_trace.Info("Creating DefaultStepHost for REPL execution (infrastructure step)");
|
|
||||||
return _hostContext.CreateService<IDefaultStepHost>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var container = context?.Global?.Container;
|
|
||||||
if (container != null)
|
|
||||||
{
|
|
||||||
// Container hooks don't always set ContainerId, but the container
|
|
||||||
// step host handles that internally
|
|
||||||
var hooksEnabled = FeatureManager.IsContainerHooksEnabled(context.Global?.Variables);
|
|
||||||
if (hooksEnabled || !string.IsNullOrEmpty(container.ContainerId))
|
|
||||||
{
|
|
||||||
_trace.Info("Creating ContainerStepHost for REPL execution");
|
|
||||||
var containerStepHost = _hostContext.CreateService<IContainerStepHost>();
|
|
||||||
containerStepHost.Container = container;
|
|
||||||
return containerStepHost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_trace.Info("Creating DefaultStepHost for REPL execution");
|
|
||||||
return _hostContext.CreateService<IDefaultStepHost>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
||||||
|
|
||||||
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.
|
|
||||||
private 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;
|
|
||||||
|
|
||||||
private const string _redactedValue = "***";
|
|
||||||
|
|
||||||
private readonly ISecretMasker _secretMasker;
|
|
||||||
|
|
||||||
// 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(ISecretMasker secretMasker)
|
|
||||||
{
|
|
||||||
_secretMasker = secretMasker ?? throw new ArgumentNullException(nameof(secretMasker));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 (1–100) 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>
|
|
||||||
/// 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 = _secretMasker.MaskSecrets(result ?? "null");
|
|
||||||
|
|
||||||
return new EvaluateResponseBody
|
|
||||||
{
|
|
||||||
Result = result,
|
|
||||||
Type = InferResultType(result),
|
|
||||||
VariablesReference = 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var errorMessage = _secretMasker.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)
|
|
||||||
{
|
|
||||||
value = value?.ToLower();
|
|
||||||
if (value == null || value == "null")
|
|
||||||
return "null";
|
|
||||||
if (value == "true" || value == "false")
|
|
||||||
return "boolean";
|
|
||||||
if (double.TryParse(value, NumberStyles.Any,
|
|
||||||
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} }}}}"
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
variable.Value = "null";
|
|
||||||
variable.Type = "null";
|
|
||||||
variable.VariablesReference = 0;
|
|
||||||
return variable;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (value)
|
|
||||||
{
|
|
||||||
case StringContextData str:
|
|
||||||
variable.Value = _secretMasker.MaskSecrets(str.Value);
|
|
||||||
variable.Type = "string";
|
|
||||||
variable.VariablesReference = 0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NumberContextData num:
|
|
||||||
variable.Value = _secretMasker.MaskSecrets(num.Value.ToString("G15", CultureInfo.InvariantCulture));
|
|
||||||
variable.Type = "number";
|
|
||||||
variable.VariablesReference = 0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BooleanContextData boolVal:
|
|
||||||
variable.Value = 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 = _secretMasker.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using GitHub.DistributedTask.Pipelines;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Dap
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Consolidated runtime configuration for the job debugger.
|
|
||||||
/// Populated once from the acquire response and owned by <see cref="GlobalContext"/>.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class DebuggerConfig
|
|
||||||
{
|
|
||||||
public DebuggerConfig(bool enabled, DebuggerTunnelInfo tunnel, bool overrideWelcomeMessage = false, string welcomeMessage = null)
|
|
||||||
{
|
|
||||||
Enabled = enabled;
|
|
||||||
Tunnel = tunnel;
|
|
||||||
OverrideWelcomeMessage = overrideWelcomeMessage;
|
|
||||||
WelcomeMessage = welcomeMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Whether the debugger is enabled for this job.</summary>
|
|
||||||
public bool Enabled { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dev Tunnel details for remote debugging.
|
|
||||||
/// Required when <see cref="Enabled"/> is true.
|
|
||||||
/// </summary>
|
|
||||||
public DebuggerTunnelInfo Tunnel { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When true, the runner overrides the default welcome message with
|
|
||||||
/// <see cref="WelcomeMessage"/>. A null or empty <see cref="WelcomeMessage"/>
|
|
||||||
/// suppresses the message entirely. When false, the default help text is shown.
|
|
||||||
/// </summary>
|
|
||||||
public bool OverrideWelcomeMessage { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional welcome message content for the debugger console. Only used when
|
|
||||||
/// <see cref="OverrideWelcomeMessage"/> is true.
|
|
||||||
/// </summary>
|
|
||||||
public string WelcomeMessage { get; }
|
|
||||||
|
|
||||||
/// <summary>Whether the tunnel configuration is complete and valid.</summary>
|
|
||||||
public bool HasValidTunnel => Tunnel != null
|
|
||||||
&& !string.IsNullOrEmpty(Tunnel.TunnelId)
|
|
||||||
&& !string.IsNullOrEmpty(Tunnel.ClusterId)
|
|
||||||
&& !string.IsNullOrEmpty(Tunnel.HostToken)
|
|
||||||
&& Tunnel.Port >= 1024 && Tunnel.Port <= 65535;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using GitHub.Runner.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Dap
|
|
||||||
{
|
|
||||||
public enum DapSessionState
|
|
||||||
{
|
|
||||||
NotStarted,
|
|
||||||
WaitingForConnection,
|
|
||||||
Initializing,
|
|
||||||
Ready,
|
|
||||||
Paused,
|
|
||||||
Running,
|
|
||||||
Terminated
|
|
||||||
}
|
|
||||||
|
|
||||||
[ServiceLocator(Default = typeof(DapDebugger))]
|
|
||||||
public interface IDapDebugger : IRunnerService
|
|
||||||
{
|
|
||||||
Task StartAsync(IExecutionContext jobContext);
|
|
||||||
Task WaitUntilReadyAsync();
|
|
||||||
Task OnJobStepsInitializedAsync(IEnumerable<IStep> steps, IEnumerable<IStep> initialPostSteps);
|
|
||||||
void OnPostStepRegistered(IStep step);
|
|
||||||
Task OnStepStartingAsync(IStep step);
|
|
||||||
void OnStepCompleted(IStep step);
|
|
||||||
Task OnJobCompletedAsync();
|
|
||||||
Task StopAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using GitHub.Runner.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Dap
|
|
||||||
{
|
|
||||||
[ServiceLocator(Default = typeof(WebSocketDapBridge))]
|
|
||||||
public interface IWebSocketDapBridge : IRunnerService
|
|
||||||
{
|
|
||||||
void Start(int listenPort, int targetPort);
|
|
||||||
Task ShutdownAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Dap
|
|
||||||
{
|
|
||||||
internal sealed class JobExecutionView
|
|
||||||
{
|
|
||||||
private const string _sourceFileName = "execution.yml";
|
|
||||||
|
|
||||||
private readonly object _lock = new object();
|
|
||||||
private readonly List<SourceEntry> _preEntries = new List<SourceEntry>();
|
|
||||||
private readonly List<SourceEntry> _mainEntries = new List<SourceEntry>();
|
|
||||||
private readonly List<SourceEntry> _postEntries = new List<SourceEntry>();
|
|
||||||
private readonly List<StepLine> _lineByStep = new List<StepLine>();
|
|
||||||
private string _content;
|
|
||||||
private int _completeJobLine;
|
|
||||||
|
|
||||||
public JobExecutionView(
|
|
||||||
string jobId,
|
|
||||||
IEnumerable<IStep> steps,
|
|
||||||
IEnumerable<IStep> initialPostSteps,
|
|
||||||
IEnumerable<PredictedPostStep> predictedPostSteps = null)
|
|
||||||
{
|
|
||||||
JobId = string.IsNullOrWhiteSpace(jobId) ? "job" : jobId;
|
|
||||||
|
|
||||||
_preEntries.Add(new SourceEntry("Set up job"));
|
|
||||||
AddSteps(steps);
|
|
||||||
AddPredictedPostSteps(predictedPostSteps);
|
|
||||||
AddSteps(initialPostSteps);
|
|
||||||
_postEntries.Add(SourceEntry.CreateSyntheticCompleteJob());
|
|
||||||
Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string JobId { get; }
|
|
||||||
public string SourceFileName => _sourceFileName;
|
|
||||||
|
|
||||||
public string Content
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int CompleteJobLine
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _completeJobLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int? TryClaimPredictedStep(string matchKey, IStep step)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(matchKey) || step == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
var existingLine = TryGetLineForStepNoLock(step);
|
|
||||||
if (existingLine.HasValue)
|
|
||||||
{
|
|
||||||
return existingLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entry in _postEntries)
|
|
||||||
{
|
|
||||||
if (!string.Equals(entry.MatchKey, matchKey, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.Step != null && !ReferenceEquals(entry.Step, step))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.Step = step;
|
|
||||||
Render();
|
|
||||||
return TryGetLineForStepNoLock(step);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int? TryGetLineForStep(IStep step)
|
|
||||||
{
|
|
||||||
if (step == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return TryGetLineForStepNoLock(step);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int? TryGetLineForStepNoLock(IStep step)
|
|
||||||
{
|
|
||||||
foreach (var stepLine in _lineByStep)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(stepLine.Step, step))
|
|
||||||
{
|
|
||||||
return stepLine.Line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddSteps(IEnumerable<IStep> steps)
|
|
||||||
{
|
|
||||||
if (steps == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var step in steps)
|
|
||||||
{
|
|
||||||
if (step == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
GetEntries(GetSection(step)).Add(new SourceEntry(step));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddPredictedPostSteps(IEnumerable<PredictedPostStep> steps)
|
|
||||||
{
|
|
||||||
if (steps == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var step in steps)
|
|
||||||
{
|
|
||||||
if (step == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_postEntries.Add(new SourceEntry(step.DisplayName, step.MatchKey));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<SourceEntry> GetEntries(SourceSection section)
|
|
||||||
{
|
|
||||||
switch (section)
|
|
||||||
{
|
|
||||||
case SourceSection.Pre:
|
|
||||||
return _preEntries;
|
|
||||||
case SourceSection.Post:
|
|
||||||
return _postEntries;
|
|
||||||
default:
|
|
||||||
return _mainEntries;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SourceSection GetSection(IStep step)
|
|
||||||
{
|
|
||||||
if (step is IActionRunner actionRunner)
|
|
||||||
{
|
|
||||||
return GetSection(actionRunner.Stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step.ExecutionContext != null)
|
|
||||||
{
|
|
||||||
return GetSection(step.ExecutionContext.Stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SourceSection.Main;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SourceSection GetSection(ActionRunStage stage)
|
|
||||||
{
|
|
||||||
switch (stage)
|
|
||||||
{
|
|
||||||
case ActionRunStage.Pre:
|
|
||||||
return SourceSection.Pre;
|
|
||||||
case ActionRunStage.Post:
|
|
||||||
return SourceSection.Post;
|
|
||||||
default:
|
|
||||||
return SourceSection.Main;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Render()
|
|
||||||
{
|
|
||||||
_lineByStep.Clear();
|
|
||||||
_completeJobLine = 0;
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
var line = 1;
|
|
||||||
|
|
||||||
AppendSection(sb, "pre", _preEntries, ref line, appendSeparatorLine: true);
|
|
||||||
AppendSection(sb, "main", _mainEntries, ref line, appendSeparatorLine: true);
|
|
||||||
AppendSection(sb, "post", _postEntries, ref line, appendSeparatorLine: false);
|
|
||||||
|
|
||||||
_content = sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendSection(
|
|
||||||
StringBuilder sb,
|
|
||||||
string sectionName,
|
|
||||||
IReadOnlyList<SourceEntry> entries,
|
|
||||||
ref int line,
|
|
||||||
bool appendSeparatorLine)
|
|
||||||
{
|
|
||||||
sb.Append(sectionName).Append(":\n");
|
|
||||||
line++;
|
|
||||||
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
if (entry.Step != null && TryGetLineForStepNoLock(entry.Step) == null)
|
|
||||||
{
|
|
||||||
_lineByStep.Add(new StepLine(entry.Step, line));
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append(" - step: ");
|
|
||||||
sb.Append(FormatYamlString(entry.DisplayName));
|
|
||||||
sb.Append('\n');
|
|
||||||
if (entry.IsSyntheticCompleteJob)
|
|
||||||
{
|
|
||||||
_completeJobLine = line;
|
|
||||||
}
|
|
||||||
|
|
||||||
line++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appendSeparatorLine)
|
|
||||||
{
|
|
||||||
sb.Append('\n');
|
|
||||||
line++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatYamlString(string value)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.Append('"');
|
|
||||||
foreach (var c in value)
|
|
||||||
{
|
|
||||||
switch (c)
|
|
||||||
{
|
|
||||||
case '\\':
|
|
||||||
sb.Append(@"\\");
|
|
||||||
break;
|
|
||||||
case '"':
|
|
||||||
sb.Append("\\\"");
|
|
||||||
break;
|
|
||||||
case '\r':
|
|
||||||
sb.Append(@"\r");
|
|
||||||
break;
|
|
||||||
case '\n':
|
|
||||||
sb.Append(@"\n");
|
|
||||||
break;
|
|
||||||
case '\t':
|
|
||||||
sb.Append(@"\t");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (char.IsControl(c))
|
|
||||||
{
|
|
||||||
sb.Append(@"\u");
|
|
||||||
sb.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.Append(c);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append('"');
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class PredictedPostStep
|
|
||||||
{
|
|
||||||
public PredictedPostStep(string displayName, string matchKey)
|
|
||||||
{
|
|
||||||
DisplayName = string.IsNullOrEmpty(displayName) ? "step" : displayName;
|
|
||||||
MatchKey = matchKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string DisplayName { get; }
|
|
||||||
public string MatchKey { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class StepLine
|
|
||||||
{
|
|
||||||
public StepLine(IStep step, int line)
|
|
||||||
{
|
|
||||||
Step = step;
|
|
||||||
Line = line;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IStep Step { get; }
|
|
||||||
public int Line { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class SourceEntry
|
|
||||||
{
|
|
||||||
public SourceEntry(string displayName)
|
|
||||||
{
|
|
||||||
DisplayName = string.IsNullOrEmpty(displayName) ? "step" : displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SourceEntry(string displayName, string matchKey)
|
|
||||||
: this(displayName)
|
|
||||||
{
|
|
||||||
MatchKey = matchKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SourceEntry(IStep step)
|
|
||||||
{
|
|
||||||
Step = step;
|
|
||||||
DisplayName = string.IsNullOrEmpty(step.DisplayName) ? "step" : step.DisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SourceEntry(string displayName, bool isSyntheticCompleteJob)
|
|
||||||
: this(displayName)
|
|
||||||
{
|
|
||||||
IsSyntheticCompleteJob = isSyntheticCompleteJob;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SourceEntry CreateSyntheticCompleteJob()
|
|
||||||
{
|
|
||||||
return new SourceEntry("Complete job", isSyntheticCompleteJob: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IStep Step { get; set; }
|
|
||||||
public string DisplayName { get; }
|
|
||||||
public string MatchKey { get; }
|
|
||||||
public bool IsSyntheticCompleteJob { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum SourceSection
|
|
||||||
{
|
|
||||||
Pre,
|
|
||||||
Main,
|
|
||||||
Post
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,839 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using GitHub.Runner.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Dap
|
|
||||||
{
|
|
||||||
internal sealed class WebSocketDapBridge : RunnerService, IWebSocketDapBridge
|
|
||||||
{
|
|
||||||
internal enum IncomingStreamPrefixKind
|
|
||||||
{
|
|
||||||
Unknown,
|
|
||||||
HttpWebSocketUpgrade,
|
|
||||||
PreUpgradedWebSocket,
|
|
||||||
WebSocketReservedBits,
|
|
||||||
Http2Preface,
|
|
||||||
TlsClientHello,
|
|
||||||
}
|
|
||||||
|
|
||||||
private const int _bufferSize = 32 * 1024;
|
|
||||||
private const int _maxHeaderLineLength = 8 * 1024;
|
|
||||||
private const int _defaultMaxInboundMessageSize = 10 * 1024 * 1024; // 10 MB
|
|
||||||
private static readonly TimeSpan _keepAliveInterval = TimeSpan.FromSeconds(30);
|
|
||||||
private static readonly TimeSpan _closeTimeout = TimeSpan.FromSeconds(5);
|
|
||||||
private static readonly TimeSpan _handshakeTimeout = TimeSpan.FromSeconds(10);
|
|
||||||
private const string _webSocketAcceptMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
||||||
private const int _maxHeaderCount = 64;
|
|
||||||
private static readonly byte[] _headerEndMarker = new byte[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
|
|
||||||
|
|
||||||
private int _listenPort;
|
|
||||||
private int _targetPort;
|
|
||||||
|
|
||||||
private TcpListener _listener;
|
|
||||||
private CancellationTokenSource _loopCts;
|
|
||||||
private Task _acceptLoopTask;
|
|
||||||
|
|
||||||
public int MaxInboundMessageSize { get; set; } = _defaultMaxInboundMessageSize;
|
|
||||||
|
|
||||||
internal int ListenPort => (_listener?.LocalEndpoint as IPEndPoint)?.Port ?? 0;
|
|
||||||
|
|
||||||
public void Start(int listenPort, int targetPort)
|
|
||||||
{
|
|
||||||
if (_listener != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("WebSocket DAP bridge already started.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_listenPort = listenPort;
|
|
||||||
_targetPort = targetPort;
|
|
||||||
|
|
||||||
_listener = new TcpListener(IPAddress.Loopback, _listenPort);
|
|
||||||
_listener.Start();
|
|
||||||
_loopCts = new CancellationTokenSource();
|
|
||||||
_acceptLoopTask = AcceptLoopAsync(_loopCts.Token);
|
|
||||||
|
|
||||||
Trace.Info($"WebSocket DAP bridge listening on {_listener.LocalEndpoint} -> 127.0.0.1:{_targetPort}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ShutdownAsync()
|
|
||||||
{
|
|
||||||
_loopCts?.Cancel();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_listener?.Stop();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Warning($"Error stopping listener during shutdown ({ex.GetType().Name})");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_acceptLoopTask != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _acceptLoopTask;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// expected on shutdown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_loopCts?.Dispose();
|
|
||||||
_loopCts = null;
|
|
||||||
_listener = null;
|
|
||||||
_acceptLoopTask = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
TcpClient client = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
client = await _listener.AcceptTcpClientAsync(cancellationToken);
|
|
||||||
client.NoDelay = true;
|
|
||||||
await HandleClientAsync(client, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
client?.Dispose();
|
|
||||||
Trace.Error($"WebSocket DAP bridge connection error");
|
|
||||||
Trace.Error(ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
client?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace.Info("WebSocket DAP bridge accept loop ended");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleClientAsync(TcpClient incomingClient, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using (var incomingStream = incomingClient.GetStream())
|
|
||||||
{
|
|
||||||
Trace.Info($"WebSocket DAP bridge accepted client {incomingClient.Client.RemoteEndPoint}");
|
|
||||||
|
|
||||||
WebSocket webSocket;
|
|
||||||
using (var handshakeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
|
||||||
{
|
|
||||||
handshakeCts.CancelAfter(_handshakeTimeout);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
webSocket = await AcceptWebSocketAsync(incomingStream, handshakeCts.Token);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
Trace.Warning("WebSocket handshake timed out");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (webSocket == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (webSocket)
|
|
||||||
using (var dapClient = new TcpClient())
|
|
||||||
{
|
|
||||||
dapClient.NoDelay = true;
|
|
||||||
await dapClient.ConnectAsync(IPAddress.Loopback, _targetPort, cancellationToken);
|
|
||||||
|
|
||||||
using (var dapStream = dapClient.GetStream())
|
|
||||||
using (var sessionCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
|
||||||
{
|
|
||||||
var proxyToken = sessionCts.Token;
|
|
||||||
var wsToTcpTask = PumpWebSocketToTcpAsync(webSocket, dapStream, proxyToken);
|
|
||||||
var tcpToWsTask = PumpTcpToWebSocketAsync(dapStream, webSocket, proxyToken);
|
|
||||||
|
|
||||||
await Task.WhenAny(wsToTcpTask, tcpToWsTask);
|
|
||||||
sessionCts.Cancel();
|
|
||||||
|
|
||||||
await CloseWebSocketAsync(webSocket);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.WhenAll(wsToTcpTask, tcpToWsTask);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (proxyToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
// expected during shutdown
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Warning($"DAP protocol error: {ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<WebSocket> AcceptWebSocketAsync(NetworkStream stream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var initialBytes = await ReadInitialBytesAsync(stream, cancellationToken);
|
|
||||||
if (initialBytes == null || initialBytes.Length == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var prefixKind = ClassifyIncomingStreamPrefix(initialBytes);
|
|
||||||
if (prefixKind == IncomingStreamPrefixKind.PreUpgradedWebSocket)
|
|
||||||
{
|
|
||||||
Trace.Info($"Treating incoming tunnel stream as an already-upgraded websocket connection ({DescribeInitialBytes(initialBytes)})");
|
|
||||||
return WebSocket.CreateFromStream(
|
|
||||||
new ReplayableStream(stream, initialBytes),
|
|
||||||
isServer: true,
|
|
||||||
subProtocol: null,
|
|
||||||
keepAliveInterval: _keepAliveInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prefixKind != IncomingStreamPrefixKind.HttpWebSocketUpgrade)
|
|
||||||
{
|
|
||||||
Trace.Warning($"Unsupported debugger tunnel stream prefix ({prefixKind}): {DescribeInitialBytes(initialBytes)}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var handshakeStream = new ReplayableStream(stream, initialBytes);
|
|
||||||
var requestLine = await ReadLineAsync(handshakeStream, cancellationToken);
|
|
||||||
if (string.IsNullOrEmpty(requestLine))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
if (headers.Count >= _maxHeaderCount)
|
|
||||||
{
|
|
||||||
Trace.Warning($"Rejected WebSocket request with too many headers (>{_maxHeaderCount})");
|
|
||||||
await WriteHttpErrorAsync(stream, HttpStatusCode.BadRequest, "Too many headers.", cancellationToken);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var line = await ReadLineAsync(handshakeStream, cancellationToken);
|
|
||||||
if (line == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.Length == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var separatorIndex = line.IndexOf(':');
|
|
||||||
if (separatorIndex <= 0)
|
|
||||||
{
|
|
||||||
await WriteHttpErrorAsync(stream, HttpStatusCode.BadRequest, "Invalid HTTP header.", cancellationToken);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var headerName = line.Substring(0, separatorIndex).Trim();
|
|
||||||
var headerValue = line.Substring(separatorIndex + 1).Trim();
|
|
||||||
|
|
||||||
if (headers.TryGetValue(headerName, out var existingValue))
|
|
||||||
{
|
|
||||||
headers[headerName] = $"{existingValue}, {headerValue}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
headers[headerName] = headerValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsValidWebSocketRequest(requestLine, headers))
|
|
||||||
{
|
|
||||||
var method = requestLine.Split(' ')[0];
|
|
||||||
Trace.Info($"Rejected non-websocket request (method={method})");
|
|
||||||
await WriteHttpErrorAsync(stream, HttpStatusCode.BadRequest, "Expected a websocket upgrade request.", cancellationToken);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!headers.TryGetValue("Sec-WebSocket-Version", out var webSocketVersion) ||
|
|
||||||
!string.Equals(webSocketVersion.Trim(), "13", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
Trace.Warning("Rejected WebSocket request with unsupported version");
|
|
||||||
await WriteHttpErrorAsync(stream, (HttpStatusCode)426, "Unsupported WebSocket version. Expected: 13.", cancellationToken);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var webSocketKey = headers["Sec-WebSocket-Key"];
|
|
||||||
if (!IsValidWebSocketKey(webSocketKey))
|
|
||||||
{
|
|
||||||
Trace.Warning("Rejected WebSocket request with invalid Sec-WebSocket-Key");
|
|
||||||
await WriteHttpErrorAsync(stream, HttpStatusCode.BadRequest, "Invalid Sec-WebSocket-Key.", cancellationToken);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var acceptValue = ComputeAcceptValue(webSocketKey);
|
|
||||||
var responseBytes = Encoding.ASCII.GetBytes(
|
|
||||||
"HTTP/1.1 101 Switching Protocols\r\n" +
|
|
||||||
"Connection: Upgrade\r\n" +
|
|
||||||
"Upgrade: websocket\r\n" +
|
|
||||||
$"Sec-WebSocket-Accept: {acceptValue}\r\n" +
|
|
||||||
"\r\n");
|
|
||||||
|
|
||||||
await handshakeStream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
|
||||||
await handshakeStream.FlushAsync(cancellationToken);
|
|
||||||
|
|
||||||
Trace.Info("WebSocket DAP bridge completed websocket handshake");
|
|
||||||
return WebSocket.CreateFromStream(handshakeStream, isServer: true, subProtocol: null, keepAliveInterval: _keepAliveInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PumpWebSocketToTcpAsync(WebSocket source, NetworkStream destination, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var buffer = new byte[_bufferSize];
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
using (var messageStream = new MemoryStream())
|
|
||||||
{
|
|
||||||
WebSocketReceiveResult result;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
|
|
||||||
if (result.MessageType == WebSocketMessageType.Close)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.MessageType != WebSocketMessageType.Binary &&
|
|
||||||
result.MessageType != WebSocketMessageType.Text)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Count > 0)
|
|
||||||
{
|
|
||||||
if (messageStream.Length + result.Count > MaxInboundMessageSize)
|
|
||||||
{
|
|
||||||
Trace.Warning($"WebSocket message exceeds maximum allowed size of {MaxInboundMessageSize} bytes, closing connection");
|
|
||||||
await source.CloseAsync(
|
|
||||||
WebSocketCloseStatus.MessageTooBig,
|
|
||||||
$"Message exceeds {MaxInboundMessageSize} byte limit",
|
|
||||||
CancellationToken.None);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageStream.Write(buffer, 0, result.Count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (!result.EndOfMessage && !cancellationToken.IsCancellationRequested);
|
|
||||||
|
|
||||||
if (result.MessageType != WebSocketMessageType.Binary &&
|
|
||||||
result.MessageType != WebSocketMessageType.Text)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageBytes = messageStream.ToArray();
|
|
||||||
if (messageBytes.Length == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentLengthHeader = Encoding.ASCII.GetBytes($"Content-Length: {messageBytes.Length}\r\n\r\n");
|
|
||||||
await destination.WriteAsync(contentLengthHeader, 0, contentLengthHeader.Length, cancellationToken);
|
|
||||||
await destination.WriteAsync(messageBytes, 0, messageBytes.Length, cancellationToken);
|
|
||||||
await destination.FlushAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task PumpTcpToWebSocketAsync(NetworkStream source, WebSocket destination, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var readBuffer = new byte[_bufferSize];
|
|
||||||
var dapBuffer = new List<byte>();
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
var bytesRead = await source.ReadAsync(readBuffer, 0, readBuffer.Length, cancellationToken);
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
dapBuffer.AddRange(new ArraySegment<byte>(readBuffer, 0, bytesRead));
|
|
||||||
|
|
||||||
while (TryParseDapMessage(dapBuffer, out var messageBody))
|
|
||||||
{
|
|
||||||
await destination.SendAsync(
|
|
||||||
new ArraySegment<byte>(messageBody),
|
|
||||||
WebSocketMessageType.Text,
|
|
||||||
endOfMessage: true,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseDapMessage(List<byte> buffer, out byte[] messageBody)
|
|
||||||
{
|
|
||||||
messageBody = null;
|
|
||||||
|
|
||||||
var headerEndIndex = FindSequence(buffer, _headerEndMarker);
|
|
||||||
if (headerEndIndex == -1)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var headerBytes = buffer.GetRange(0, headerEndIndex).ToArray();
|
|
||||||
var headerText = Encoding.ASCII.GetString(headerBytes);
|
|
||||||
|
|
||||||
var contentLength = -1;
|
|
||||||
foreach (var line in headerText.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var valueStart = line.IndexOf(':') + 1;
|
|
||||||
if (int.TryParse(line.Substring(valueStart).Trim(), out var parsedLength))
|
|
||||||
{
|
|
||||||
contentLength = parsedLength;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentLength < 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("DAP message missing or unparseable Content-Length header; tearing down session.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageStart = headerEndIndex + 4;
|
|
||||||
var messageEnd = messageStart + contentLength;
|
|
||||||
|
|
||||||
if (buffer.Count < messageEnd)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageBody = buffer.GetRange(messageStart, contentLength).ToArray();
|
|
||||||
buffer.RemoveRange(0, messageEnd);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int FindSequence(List<byte> buffer, byte[] sequence)
|
|
||||||
{
|
|
||||||
if (buffer.Count < sequence.Length)
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i <= buffer.Count - sequence.Length; i++)
|
|
||||||
{
|
|
||||||
var match = true;
|
|
||||||
for (int j = 0; j < sequence.Length; j++)
|
|
||||||
{
|
|
||||||
if (buffer[i + j] != sequence[j])
|
|
||||||
{
|
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match)
|
|
||||||
{
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsValidWebSocketRequest(string requestLine, IDictionary<string, string> headers)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(requestLine))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestLineParts = requestLine.Split(' ');
|
|
||||||
if (requestLineParts.Length < 3 || !string.Equals(requestLineParts[0], "GET", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return HeaderContainsToken(headers, "Connection", "Upgrade") &&
|
|
||||||
HeaderContainsToken(headers, "Upgrade", "websocket") &&
|
|
||||||
headers.ContainsKey("Sec-WebSocket-Key");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HeaderContainsToken(IDictionary<string, string> headers, string headerName, string expectedToken)
|
|
||||||
{
|
|
||||||
if (!headers.TryGetValue(headerName, out var headerValue) || string.IsNullOrWhiteSpace(headerValue))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return headerValue
|
|
||||||
.Split(',')
|
|
||||||
.Select(token => token.Trim())
|
|
||||||
.Any(token => string.Equals(token, expectedToken, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ComputeAcceptValue(string webSocketKey)
|
|
||||||
{
|
|
||||||
using (var sha1 = SHA1.Create())
|
|
||||||
{
|
|
||||||
var inputBytes = Encoding.ASCII.GetBytes($"{webSocketKey}{_webSocketAcceptMagic}");
|
|
||||||
var hashBytes = sha1.ComputeHash(inputBytes);
|
|
||||||
return Convert.ToBase64String(hashBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsValidWebSocketKey(string key)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(key) || key.IndexOfAny(new[] { '\r', '\n' }) >= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var decoded = Convert.FromBase64String(key);
|
|
||||||
return decoded.Length == 16;
|
|
||||||
}
|
|
||||||
catch (FormatException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> ReadLineAsync(Stream stream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var lineBuilder = new StringBuilder();
|
|
||||||
var buffer = new byte[1];
|
|
||||||
var previousWasCarriageReturn = false;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var bytesRead = await stream.ReadAsync(buffer, 0, 1, cancellationToken);
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
return lineBuilder.Length > 0 ? lineBuilder.ToString() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentChar = (char)buffer[0];
|
|
||||||
if (currentChar == '\n' && previousWasCarriageReturn)
|
|
||||||
{
|
|
||||||
if (lineBuilder.Length > 0 && lineBuilder[lineBuilder.Length - 1] == '\r')
|
|
||||||
{
|
|
||||||
lineBuilder.Length--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lineBuilder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
previousWasCarriageReturn = currentChar == '\r';
|
|
||||||
lineBuilder.Append(currentChar);
|
|
||||||
|
|
||||||
if (lineBuilder.Length > _maxHeaderLineLength)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"HTTP header line exceeds maximum length of {_maxHeaderLineLength}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<byte[]> ReadInitialBytesAsync(NetworkStream stream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var buffer = new byte[4];
|
|
||||||
var totalRead = 0;
|
|
||||||
|
|
||||||
while (totalRead < buffer.Length)
|
|
||||||
{
|
|
||||||
var bytesRead = await stream.ReadAsync(buffer, totalRead, buffer.Length - totalRead, cancellationToken);
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalRead += bytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalRead == 0)
|
|
||||||
{
|
|
||||||
return Array.Empty<byte>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalRead == buffer.Length)
|
|
||||||
{
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
var initialBytes = new byte[totalRead];
|
|
||||||
Array.Copy(buffer, initialBytes, totalRead);
|
|
||||||
return initialBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static IncomingStreamPrefixKind ClassifyIncomingStreamPrefix(byte[] initialBytes)
|
|
||||||
{
|
|
||||||
if (LooksLikeHttpUpgrade(initialBytes))
|
|
||||||
{
|
|
||||||
return IncomingStreamPrefixKind.HttpWebSocketUpgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LooksLikeHttp2Preface(initialBytes))
|
|
||||||
{
|
|
||||||
return IncomingStreamPrefixKind.Http2Preface;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LooksLikeTlsClientHello(initialBytes))
|
|
||||||
{
|
|
||||||
return IncomingStreamPrefixKind.TlsClientHello;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LooksLikeWebSocketFramePrefix(initialBytes, requireReservedBitsClear: false))
|
|
||||||
{
|
|
||||||
return HasReservedBitsSet(initialBytes[0])
|
|
||||||
? IncomingStreamPrefixKind.WebSocketReservedBits
|
|
||||||
: IncomingStreamPrefixKind.PreUpgradedWebSocket;
|
|
||||||
}
|
|
||||||
|
|
||||||
return IncomingStreamPrefixKind.Unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string DescribeInitialBytes(byte[] initialBytes)
|
|
||||||
{
|
|
||||||
if (initialBytes == null || initialBytes.Length == 0)
|
|
||||||
{
|
|
||||||
return "no bytes read";
|
|
||||||
}
|
|
||||||
|
|
||||||
var hex = BitConverter.ToString(initialBytes);
|
|
||||||
var ascii = new string(initialBytes.Select(value => value >= 32 && value <= 126 ? (char)value : '.').ToArray());
|
|
||||||
return $"hex={hex}, ascii=\"{ascii}\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool LooksLikeHttpUpgrade(byte[] initialBytes)
|
|
||||||
{
|
|
||||||
if (initialBytes == null || initialBytes.Length < 4)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialBytes[0] == (byte)'G' &&
|
|
||||||
initialBytes[1] == (byte)'E' &&
|
|
||||||
initialBytes[2] == (byte)'T' &&
|
|
||||||
initialBytes[3] == (byte)' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool LooksLikeHttp2Preface(byte[] initialBytes)
|
|
||||||
{
|
|
||||||
if (initialBytes == null || initialBytes.Length < 4)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialBytes[0] == (byte)'P' &&
|
|
||||||
initialBytes[1] == (byte)'R' &&
|
|
||||||
initialBytes[2] == (byte)'I' &&
|
|
||||||
initialBytes[3] == (byte)' ';
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool LooksLikeTlsClientHello(byte[] initialBytes)
|
|
||||||
{
|
|
||||||
if (initialBytes == null || initialBytes.Length < 3)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialBytes[0] == 0x16 &&
|
|
||||||
initialBytes[1] == 0x03 &&
|
|
||||||
initialBytes[2] >= 0x00 &&
|
|
||||||
initialBytes[2] <= 0x04;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool LooksLikeWebSocketFramePrefix(byte[] initialBytes, bool requireReservedBitsClear)
|
|
||||||
{
|
|
||||||
if (initialBytes == null || initialBytes.Length < 2)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstByte = initialBytes[0];
|
|
||||||
var secondByte = initialBytes[1];
|
|
||||||
var opcode = firstByte & 0x0F;
|
|
||||||
var isMasked = (secondByte & 0x80) != 0;
|
|
||||||
|
|
||||||
if (!isMasked || !IsSupportedWebSocketOpcode(opcode))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !requireReservedBitsClear || !HasReservedBitsSet(firstByte);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HasReservedBitsSet(byte firstByte)
|
|
||||||
{
|
|
||||||
return (firstByte & 0x70) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSupportedWebSocketOpcode(int opcode)
|
|
||||||
{
|
|
||||||
switch (opcode)
|
|
||||||
{
|
|
||||||
case 0x0:
|
|
||||||
case 0x1:
|
|
||||||
case 0x2:
|
|
||||||
case 0x8:
|
|
||||||
case 0x9:
|
|
||||||
case 0xA:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task WriteHttpErrorAsync(
|
|
||||||
NetworkStream stream,
|
|
||||||
HttpStatusCode statusCode,
|
|
||||||
string message,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var bodyBytes = Encoding.UTF8.GetBytes(message);
|
|
||||||
var responseBytes = Encoding.ASCII.GetBytes(
|
|
||||||
$"HTTP/1.1 {(int)statusCode} {statusCode}\r\n" +
|
|
||||||
"Connection: close\r\n" +
|
|
||||||
"Content-Type: text/plain; charset=utf-8\r\n" +
|
|
||||||
$"Content-Length: {bodyBytes.Length}\r\n" +
|
|
||||||
"Sec-WebSocket-Version: 13\r\n" +
|
|
||||||
"\r\n");
|
|
||||||
|
|
||||||
await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
|
|
||||||
await stream.WriteAsync(bodyBytes, 0, bodyBytes.Length, cancellationToken);
|
|
||||||
await stream.FlushAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task CloseWebSocketAsync(WebSocket webSocket)
|
|
||||||
{
|
|
||||||
if (webSocket == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (webSocket.State != WebSocketState.Open &&
|
|
||||||
webSocket.State != WebSocketState.CloseReceived)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var cts = new CancellationTokenSource(_closeTimeout);
|
|
||||||
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cts.Token);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Graceful close timed out, abort the connection.
|
|
||||||
webSocket.Abort();
|
|
||||||
}
|
|
||||||
catch (WebSocketException)
|
|
||||||
{
|
|
||||||
// Peer already disconnected.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ReplayableStream : Stream
|
|
||||||
{
|
|
||||||
private readonly Stream _innerStream;
|
|
||||||
private readonly byte[] _prefixBytes;
|
|
||||||
private int _prefixOffset;
|
|
||||||
|
|
||||||
public ReplayableStream(Stream innerStream, byte[] prefixBytes)
|
|
||||||
{
|
|
||||||
_innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream));
|
|
||||||
_prefixBytes = prefixBytes ?? Array.Empty<byte>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanRead => _innerStream.CanRead;
|
|
||||||
public override bool CanSeek => false;
|
|
||||||
public override bool CanWrite => _innerStream.CanWrite;
|
|
||||||
public override long Length => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public override long Position
|
|
||||||
{
|
|
||||||
get => throw new NotSupportedException();
|
|
||||||
set => throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Flush() => _innerStream.Flush();
|
|
||||||
|
|
||||||
public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken);
|
|
||||||
|
|
||||||
public override int Read(byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
if (TryReadPrefix(buffer, offset, count, out var bytesRead))
|
|
||||||
{
|
|
||||||
return bytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _innerStream.Read(buffer, offset, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (TryReadPrefix(buffer, offset, count, out var bytesRead))
|
|
||||||
{
|
|
||||||
return bytesRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _innerStream.ReadAsync(buffer, offset, count, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_prefixOffset < _prefixBytes.Length)
|
|
||||||
{
|
|
||||||
var bytesToCopy = Math.Min(buffer.Length, _prefixBytes.Length - _prefixOffset);
|
|
||||||
new ReadOnlySpan<byte>(_prefixBytes, _prefixOffset, bytesToCopy).CopyTo(buffer.Span);
|
|
||||||
_prefixOffset += bytesToCopy;
|
|
||||||
return bytesToCopy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _innerStream.ReadAsync(buffer, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public override void SetLength(long value) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count);
|
|
||||||
|
|
||||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
|
|
||||||
_innerStream.WriteAsync(buffer, offset, count, cancellationToken);
|
|
||||||
|
|
||||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
|
|
||||||
_innerStream.WriteAsync(buffer, cancellationToken);
|
|
||||||
|
|
||||||
private bool TryReadPrefix(byte[] buffer, int offset, int count, out int bytesRead)
|
|
||||||
{
|
|
||||||
if (_prefixOffset >= _prefixBytes.Length)
|
|
||||||
{
|
|
||||||
bytesRead = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bytesRead = Math.Min(count, _prefixBytes.Length - _prefixOffset);
|
|
||||||
Array.Copy(_prefixBytes, _prefixOffset, buffer, offset, bytesRead);
|
|
||||||
_prefixOffset += bytesRead;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -77,23 +77,15 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
List<string> StepEnvironmentOverrides { get; }
|
List<string> StepEnvironmentOverrides { get; }
|
||||||
|
|
||||||
bool IsBackground { get; }
|
ExecutionContext Root { get; }
|
||||||
|
ExecutionContext Parent { get; }
|
||||||
IExecutionContext Root { get; }
|
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
|
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
|
||||||
void CancelToken();
|
void CancelToken();
|
||||||
IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, List<Issue> embeddedIssueCollector = null, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null, TimeSpan? timeout = null, bool isBackground = false, string backgroundControlType = null, string[] backgroundControlStepIds = null, string parallelGroupId = null);
|
IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, List<Issue> embeddedIssueCollector = null, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null, TimeSpan? timeout = null);
|
||||||
IExecutionContext CreateEmbeddedChild(string scopeName, string contextName, Guid embeddedId, ActionRunStage stage, Dictionary<string, string> intraActionState = null, string siblingScopeName = null);
|
IExecutionContext CreateEmbeddedChild(string scopeName, string contextName, Guid embeddedId, ActionRunStage stage, Dictionary<string, string> intraActionState = null, string siblingScopeName = null);
|
||||||
|
|
||||||
|
|
||||||
// Background step deferral properties
|
|
||||||
Dictionary<string, string> DeferredOutputs { get; set; }
|
|
||||||
Dictionary<string, string> DeferredEnvironmentVariables { get; set; }
|
|
||||||
List<string> DeferredPrependPath { get; set; }
|
|
||||||
bool DeferOutcomeConclusion { get; set; }
|
|
||||||
|
|
||||||
// logging
|
// logging
|
||||||
long Write(string tag, string message);
|
long Write(string tag, string message);
|
||||||
void QueueAttachFile(string type, string name, string filePath);
|
void QueueAttachFile(string type, string name, string filePath);
|
||||||
@@ -109,12 +101,6 @@ namespace GitHub.Runner.Worker
|
|||||||
void SetGitHubContext(string name, string value);
|
void SetGitHubContext(string name, string value);
|
||||||
void SetOutput(string name, string value, out string reference);
|
void SetOutput(string name, string value, out string reference);
|
||||||
void SetTimeout(TimeSpan? timeout);
|
void SetTimeout(TimeSpan? timeout);
|
||||||
|
|
||||||
// Background step deferral flush methods
|
|
||||||
void FlushDeferredOutputs();
|
|
||||||
void FlushDeferredEnvironment();
|
|
||||||
void FlushDeferredOutcomeConclusion();
|
|
||||||
|
|
||||||
void AddIssue(Issue issue, ExecutionContextLogOptions logOptions);
|
void AddIssue(Issue issue, ExecutionContextLogOptions logOptions);
|
||||||
void Progress(int percentage, string currentOperation = null);
|
void Progress(int percentage, string currentOperation = null);
|
||||||
void UpdateDetailTimelineRecord(TimelineRecord record);
|
void UpdateDetailTimelineRecord(TimelineRecord record);
|
||||||
@@ -231,9 +217,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
public bool EchoOnActionCommand { get; set; }
|
public bool EchoOnActionCommand { get; set; }
|
||||||
|
|
||||||
// Whether this step runs in the background
|
|
||||||
public bool IsBackground => _record.IsBackground;
|
|
||||||
|
|
||||||
// An embedded execution context shares the same record ID, record name, and logger
|
// An embedded execution context shares the same record ID, record name, and logger
|
||||||
// as its enclosing execution context.
|
// as its enclosing execution context.
|
||||||
public bool IsEmbedded { get; private init; }
|
public bool IsEmbedded { get; private init; }
|
||||||
@@ -268,9 +251,7 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IExecutionContext IExecutionContext.Root => Root;
|
public ExecutionContext Root
|
||||||
|
|
||||||
private ExecutionContext Root
|
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@@ -285,7 +266,13 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ExecutionContext Parent
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _parentExecutionContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public JobContext JobContext
|
public JobContext JobContext
|
||||||
{
|
{
|
||||||
@@ -297,12 +284,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
public List<string> StepEnvironmentOverrides { get; } = new List<string>();
|
public List<string> StepEnvironmentOverrides { get; } = new List<string>();
|
||||||
|
|
||||||
// Background step deferral properties
|
|
||||||
public Dictionary<string, string> DeferredOutputs { get; set; }
|
|
||||||
public Dictionary<string, string> DeferredEnvironmentVariables { get; set; }
|
|
||||||
public List<string> DeferredPrependPath { get; set; }
|
|
||||||
public bool DeferOutcomeConclusion { get; set; }
|
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
base.Initialize(hostContext);
|
base.Initialize(hostContext);
|
||||||
@@ -361,25 +342,7 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
step.ExecutionContext = Root.CreatePostChild(step.DisplayName, IntraActionState, siblingScopeName);
|
step.ExecutionContext = Root.CreatePostChild(step.DisplayName, IntraActionState, siblingScopeName);
|
||||||
if (step is JobExtensionRunner)
|
|
||||||
{
|
|
||||||
step.ExecutionContext.StepTelemetry.Type = "runner";
|
|
||||||
step.ExecutionContext.StepTelemetry.Action = step.DisplayName.ToLowerInvariant().Replace(' ', '_');
|
|
||||||
}
|
|
||||||
Root.PostJobSteps.Push(step);
|
Root.PostJobSteps.Push(step);
|
||||||
|
|
||||||
if (Root.Global.Debugger?.Enabled == true)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HostContext.GetService<Dap.IDapDebugger>().OnPostStepRegistered(step);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Warning("Failed to notify DAP debugger about registered post job step.");
|
|
||||||
Trace.Error(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IExecutionContext CreateChild(
|
public IExecutionContext CreateChild(
|
||||||
@@ -397,11 +360,7 @@ namespace GitHub.Runner.Worker
|
|||||||
CancellationTokenSource cancellationTokenSource = null,
|
CancellationTokenSource cancellationTokenSource = null,
|
||||||
Guid embeddedId = default(Guid),
|
Guid embeddedId = default(Guid),
|
||||||
string siblingScopeName = null,
|
string siblingScopeName = null,
|
||||||
TimeSpan? timeout = null,
|
TimeSpan? timeout = null)
|
||||||
bool isBackground = false,
|
|
||||||
string backgroundControlType = null,
|
|
||||||
string[] backgroundControlStepIds = null,
|
|
||||||
string parallelGroupId = null)
|
|
||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
|
|
||||||
@@ -442,24 +401,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
child.EchoOnActionCommand = EchoOnActionCommand;
|
child.EchoOnActionCommand = EchoOnActionCommand;
|
||||||
|
|
||||||
// Set background step metadata before InitializeTimelineRecord so it's included in the first update
|
|
||||||
if (isBackground || backgroundControlType != null || parallelGroupId != null)
|
|
||||||
{
|
|
||||||
child._record.IsBackground = isBackground;
|
|
||||||
child._record.BackgroundControlType = backgroundControlType;
|
|
||||||
child._record.BackgroundControlStepIds = backgroundControlStepIds;
|
|
||||||
child._record.ParallelGroupId = parallelGroupId;
|
|
||||||
|
|
||||||
// Initialize deferred state for background steps — flushed at wait/wait-all
|
|
||||||
if (isBackground)
|
|
||||||
{
|
|
||||||
child.DeferredOutputs = new Dictionary<string, string>();
|
|
||||||
child.DeferredEnvironmentVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
child.DeferredPrependPath = new List<string>();
|
|
||||||
child.DeferOutcomeConclusion = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recordOrder != null)
|
if (recordOrder != null)
|
||||||
{
|
{
|
||||||
child.InitializeTimelineRecord(_mainTimelineId, recordId, _record.Id, ExecutionContextType.Task, displayName, refName, recordOrder, embedded: isEmbedded);
|
child.InitializeTimelineRecord(_mainTimelineId, recordId, _record.Id, ExecutionContextType.Task, displayName, refName, recordOrder, embedded: isEmbedded);
|
||||||
@@ -558,7 +499,7 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
PublishStepTelemetry();
|
PublishStepTelemetry();
|
||||||
|
|
||||||
if (_record.RecordType == ExecutionContextType.Task)
|
if (_record.RecordType == "Task")
|
||||||
{
|
{
|
||||||
var stepResult = new StepResult
|
var stepResult = new StepResult
|
||||||
{
|
{
|
||||||
@@ -572,11 +513,7 @@ namespace GitHub.Runner.Worker
|
|||||||
Type = StepTelemetry?.Type,
|
Type = StepTelemetry?.Type,
|
||||||
StartedAt = _record.StartTime,
|
StartedAt = _record.StartTime,
|
||||||
CompletedAt = _record.FinishTime,
|
CompletedAt = _record.FinishTime,
|
||||||
Annotations = new List<Annotation>(),
|
Annotations = new List<Annotation>()
|
||||||
// Populate background step metadata from timeline record fields
|
|
||||||
IsBackground = _record.IsBackground,
|
|
||||||
BackgroundControlType = _record.BackgroundControlType,
|
|
||||||
BackgroundControlStepIds = _record.BackgroundControlStepIds
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_record.Issues?.ForEach(issue =>
|
_record.Issues?.ForEach(issue =>
|
||||||
@@ -595,25 +532,6 @@ namespace GitHub.Runner.Worker
|
|||||||
Global.StepsResult.Add(stepResult);
|
Global.StepsResult.Add(stepResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Global.Variables.GetBoolean(Constants.Runner.Features.SendJobLevelAnnotations) ?? false)
|
|
||||||
{
|
|
||||||
if (_record.RecordType == ExecutionContextType.Job)
|
|
||||||
{
|
|
||||||
_record.Issues?.ForEach(issue =>
|
|
||||||
{
|
|
||||||
var annotation = issue.ToAnnotation();
|
|
||||||
if (annotation != null)
|
|
||||||
{
|
|
||||||
Global.JobAnnotations.Add(annotation.Value);
|
|
||||||
if (annotation.Value.IsInfrastructureIssue && string.IsNullOrEmpty(Global.InfrastructureFailureCategory))
|
|
||||||
{
|
|
||||||
Global.InfrastructureFailureCategory = issue.Category;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Root != this)
|
if (Root != this)
|
||||||
{
|
{
|
||||||
// only dispose TokenSource for step level ExecutionContext
|
// only dispose TokenSource for step level ExecutionContext
|
||||||
@@ -622,22 +540,11 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
_logger.End();
|
_logger.End();
|
||||||
|
|
||||||
if (!DeferOutcomeConclusion)
|
UpdateGlobalStepsContext();
|
||||||
{
|
|
||||||
UpdateGlobalStepsContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.Value;
|
return Result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FlushDeferredOutcomeConclusion()
|
|
||||||
{
|
|
||||||
if (DeferOutcomeConclusion)
|
|
||||||
{
|
|
||||||
UpdateGlobalStepsContext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateGlobalStepsContext()
|
public void UpdateGlobalStepsContext()
|
||||||
{
|
{
|
||||||
// Skip if generated context name. Generated context names start with "__". After 3.2 the server will never send an empty context name.
|
// Skip if generated context name. Generated context names start with "__". After 3.2 the server will never send an empty context name.
|
||||||
@@ -713,40 +620,6 @@ namespace GitHub.Runner.Worker
|
|||||||
Global.StepsContext.SetOutput(ScopeName, ContextName, name, value, out reference);
|
Global.StepsContext.SetOutput(ScopeName, ContextName, name, value, out reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FlushDeferredOutputs()
|
|
||||||
{
|
|
||||||
if (DeferredOutputs == null || DeferredOutputs.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var kvp in DeferredOutputs)
|
|
||||||
{
|
|
||||||
Global.StepsContext.SetOutput(ScopeName, ContextName, kvp.Key, kvp.Value, out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void FlushDeferredEnvironment()
|
|
||||||
{
|
|
||||||
if (DeferredEnvironmentVariables != null)
|
|
||||||
{
|
|
||||||
foreach (var kvp in DeferredEnvironmentVariables)
|
|
||||||
{
|
|
||||||
Global.EnvironmentVariables[kvp.Key] = kvp.Value;
|
|
||||||
SetEnvContext(kvp.Key, kvp.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DeferredPrependPath != null)
|
|
||||||
{
|
|
||||||
foreach (var path in DeferredPrependPath)
|
|
||||||
{
|
|
||||||
Global.PrependPath.RemoveAll(x => string.Equals(x, path, StringComparison.CurrentCulture));
|
|
||||||
Global.PrependPath.Add(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTimeout(TimeSpan? timeout)
|
public void SetTimeout(TimeSpan? timeout)
|
||||||
{
|
{
|
||||||
if (timeout != null)
|
if (timeout != null)
|
||||||
@@ -964,15 +837,6 @@ namespace GitHub.Runner.Worker
|
|||||||
// Job level annotations
|
// Job level annotations
|
||||||
Global.JobAnnotations = new List<Annotation>();
|
Global.JobAnnotations = new List<Annotation>();
|
||||||
|
|
||||||
// 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
|
// Job Outputs
|
||||||
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -988,9 +852,6 @@ namespace GitHub.Runner.Worker
|
|||||||
// File table
|
// File table
|
||||||
Global.FileTable = new List<String>(message.FileTable ?? new string[0]);
|
Global.FileTable = new List<String>(message.FileTable ?? new string[0]);
|
||||||
|
|
||||||
// Workflow dependencies (lockfile pins)
|
|
||||||
Global.ActionsDependencies = message.ActionsDependencies;
|
|
||||||
|
|
||||||
// What type of job request is running (i.e. Run Service vs. pipelines)
|
// What type of job request is running (i.e. Run Service vs. pipelines)
|
||||||
Global.Variables.Set(Constants.Variables.System.JobRequestType, message.MessageType);
|
Global.Variables.Set(Constants.Variables.System.JobRequestType, message.MessageType);
|
||||||
|
|
||||||
@@ -1008,12 +869,15 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
Trace.Info("Initializing Job context");
|
Trace.Info("Initializing Job context");
|
||||||
var jobContext = new JobContext();
|
var jobContext = new JobContext();
|
||||||
ExpressionValues.TryGetValue("job", out var jobDictionary);
|
if (Global.Variables.GetBoolean(Constants.Runner.Features.AddCheckRunIdToJobContext) ?? false)
|
||||||
if (jobDictionary != null)
|
|
||||||
{
|
{
|
||||||
foreach (var pair in jobDictionary.AssertDictionary("job"))
|
ExpressionValues.TryGetValue("job", out var jobDictionary);
|
||||||
|
if (jobDictionary != null)
|
||||||
{
|
{
|
||||||
jobContext[pair.Key] = pair.Value;
|
foreach (var pair in jobDictionary.AssertDictionary("job"))
|
||||||
|
{
|
||||||
|
jobContext[pair.Key] = pair.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExpressionValues["job"] = jobContext;
|
ExpressionValues["job"] = jobContext;
|
||||||
@@ -1082,10 +946,6 @@ namespace GitHub.Runner.Worker
|
|||||||
// Verbosity (from GitHub.Step_Debug).
|
// Verbosity (from GitHub.Step_Debug).
|
||||||
Global.WriteDebug = Global.Variables.Step_Debug ?? false;
|
Global.WriteDebug = Global.Variables.Step_Debug ?? false;
|
||||||
|
|
||||||
// Debugger enabled flag (from acquire response).
|
|
||||||
var overrideDebuggerWelcomeMessage = Global.Variables.GetBoolean(Constants.Runner.Features.OverrideDebuggerWelcomeMessage) ?? false;
|
|
||||||
Global.Debugger = new Dap.DebuggerConfig(message.EnableDebugger, message.DebuggerTunnel, overrideDebuggerWelcomeMessage, message.DebuggerWelcomeMessage);
|
|
||||||
|
|
||||||
// Hook up JobServerQueueThrottling event, we will log warning on server tarpit.
|
// Hook up JobServerQueueThrottling event, we will log warning on server tarpit.
|
||||||
_jobServerQueue.JobServerQueueThrottling += JobServerQueueThrottling_EventReceived;
|
_jobServerQueue.JobServerQueueThrottling += JobServerQueueThrottling_EventReceived;
|
||||||
}
|
}
|
||||||
@@ -1443,15 +1303,12 @@ namespace GitHub.Runner.Worker
|
|||||||
Trace.Info($"Updated step result (continue on error)");
|
Trace.Info($"Updated step result (continue on error)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DeferOutcomeConclusion)
|
UpdateGlobalStepsContext();
|
||||||
{
|
|
||||||
UpdateGlobalStepsContext();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal IPipelineTemplateEvaluator ToPipelineTemplateEvaluatorInternal(bool allowServiceContainerCommand, ObjectTemplating.ITraceWriter traceWriter = null)
|
internal IPipelineTemplateEvaluator ToPipelineTemplateEvaluatorInternal(ObjectTemplating.ITraceWriter traceWriter = null)
|
||||||
{
|
{
|
||||||
return new PipelineTemplateEvaluatorWrapper(HostContext, this, allowServiceContainerCommand, traceWriter);
|
return new PipelineTemplateEvaluatorWrapper(HostContext, this, traceWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NoOp()
|
private static void NoOp()
|
||||||
@@ -1539,13 +1396,10 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
public static IPipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null)
|
public static IPipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null)
|
||||||
{
|
{
|
||||||
var allowServiceContainerCommand = (context.Global.Variables.GetBoolean(Constants.Runner.Features.ServiceContainerCommand) ?? false)
|
|
||||||
|| StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_SERVICE_CONTAINER_COMMAND"));
|
|
||||||
|
|
||||||
// Create wrapper?
|
// Create wrapper?
|
||||||
if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER")))
|
if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER")))
|
||||||
{
|
{
|
||||||
return (context as ExecutionContext).ToPipelineTemplateEvaluatorInternal(allowServiceContainerCommand, traceWriter);
|
return (context as ExecutionContext).ToPipelineTemplateEvaluatorInternal(traceWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy
|
// Legacy
|
||||||
@@ -1557,7 +1411,6 @@ namespace GitHub.Runner.Worker
|
|||||||
return new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable)
|
return new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable)
|
||||||
{
|
{
|
||||||
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
|
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
|
||||||
AllowServiceContainerCommand = allowServiceContainerCommand,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,5 +11,10 @@ namespace GitHub.Runner.Worker
|
|||||||
var isContainerHooksPathSet = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Constants.Hooks.ContainerHooksPath));
|
var isContainerHooksPathSet = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Constants.Hooks.ContainerHooksPath));
|
||||||
return isContainerHookFeatureFlagSet && isContainerHooksPathSet;
|
return isContainerHookFeatureFlagSet && isContainerHooksPathSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsContainerActionRunnerTempEnabled(Variables variables)
|
||||||
|
{
|
||||||
|
return variables?.GetBoolean(Constants.Runner.Features.ContainerActionRunnerTemp) ?? false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,16 +122,8 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (context.DeferredPrependPath != null)
|
context.Global.PrependPath.RemoveAll(x => string.Equals(x, line, StringComparison.CurrentCulture));
|
||||||
{
|
context.Global.PrependPath.Add(line);
|
||||||
context.DeferredPrependPath.RemoveAll(x => string.Equals(x, line, StringComparison.CurrentCulture));
|
|
||||||
context.DeferredPrependPath.Add(line);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Global.PrependPath.RemoveAll(x => string.Equals(x, line, StringComparison.CurrentCulture));
|
|
||||||
context.Global.PrependPath.Add(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,15 +172,8 @@ namespace GitHub.Runner.Worker
|
|||||||
string name,
|
string name,
|
||||||
string value)
|
string value)
|
||||||
{
|
{
|
||||||
if (context.DeferredEnvironmentVariables != null)
|
context.Global.EnvironmentVariables[name] = value;
|
||||||
{
|
context.SetEnvContext(name, value);
|
||||||
context.DeferredEnvironmentVariables[name] = value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Global.EnvironmentVariables[name] = value;
|
|
||||||
context.SetEnvContext(name, value);
|
|
||||||
}
|
|
||||||
context.Debug($"{name}='{value}'");
|
context.Debug($"{name}='{value}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,14 +302,7 @@ namespace GitHub.Runner.Worker
|
|||||||
var pairs = new EnvFileKeyValuePairs(context, filePath);
|
var pairs = new EnvFileKeyValuePairs(context, filePath);
|
||||||
foreach (var pair in pairs)
|
foreach (var pair in pairs)
|
||||||
{
|
{
|
||||||
if (context.DeferredOutputs != null)
|
context.SetOutput(pair.Key, pair.Value, out var reference);
|
||||||
{
|
|
||||||
context.DeferredOutputs[pair.Key] = pair.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.SetOutput(pair.Key, pair.Value, out var reference);
|
|
||||||
}
|
|
||||||
context.Debug($"Set output {pair.Key} = {pair.Value}");
|
context.Debug($"Set output {pair.Key} = {pair.Value}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using GitHub.Actions.RunService.WebApi;
|
|||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.Runner.Worker.Container;
|
||||||
using GitHub.Runner.Worker.Dap;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Sdk.RSWebApi.Contracts;
|
using Sdk.RSWebApi.Contracts;
|
||||||
|
|
||||||
@@ -28,16 +27,9 @@ namespace GitHub.Runner.Worker
|
|||||||
public StepsContext StepsContext { get; set; }
|
public StepsContext StepsContext { get; set; }
|
||||||
public Variables Variables { get; set; }
|
public Variables Variables { get; set; }
|
||||||
public bool WriteDebug { get; set; }
|
public bool WriteDebug { get; set; }
|
||||||
public DebuggerConfig Debugger { get; set; }
|
|
||||||
public string InfrastructureFailureCategory { get; set; }
|
public string InfrastructureFailureCategory { get; set; }
|
||||||
public JObject ContainerHookState { get; set; }
|
public JObject ContainerHookState { get; set; }
|
||||||
public bool HasTemplateEvaluatorMismatch { get; set; }
|
public bool HasTemplateEvaluatorMismatch { get; set; }
|
||||||
public bool HasActionManifestMismatch { get; set; }
|
public bool HasActionManifestMismatch { get; set; }
|
||||||
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; }
|
|
||||||
public IList<String> ActionsDependencies { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -227,11 +226,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
{
|
{
|
||||||
ArgUtil.NotNull(embeddedSteps, nameof(embeddedSteps));
|
ArgUtil.NotNull(embeddedSteps, nameof(embeddedSteps));
|
||||||
|
|
||||||
bool emitCompositeMarkers =
|
|
||||||
(ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.EmitCompositeMarkers) ?? false)
|
|
||||||
|| StringUtil.ConvertToBoolean(
|
|
||||||
System.Environment.GetEnvironmentVariable(Constants.Variables.Agent.EmitCompositeMarkers));
|
|
||||||
|
|
||||||
foreach (IStep step in embeddedSteps)
|
foreach (IStep step in embeddedSteps)
|
||||||
{
|
{
|
||||||
Trace.Info($"Processing embedded step: DisplayName='{step.DisplayName}'");
|
Trace.Info($"Processing embedded step: DisplayName='{step.DisplayName}'");
|
||||||
@@ -303,27 +297,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
SetStepConclusion(step, TaskResult.Failed);
|
SetStepConclusion(step, TaskResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marker ID uses the step's fully qualified context name (ScopeName.ContextName),
|
|
||||||
// which encodes the full composite nesting chain at any depth.
|
|
||||||
var markerId = emitCompositeMarkers ? step.ExecutionContext.GetFullyQualifiedContextName() : null;
|
|
||||||
var stepStopwatch = default(Stopwatch);
|
|
||||||
var endMarkerEmitted = false;
|
|
||||||
|
|
||||||
// Emit start marker after full context setup so display name expressions resolve correctly
|
|
||||||
if (emitCompositeMarkers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
step.EvaluateDisplayName(step.ExecutionContext.ExpressionValues, step.ExecutionContext, out _);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Warning("Caught exception while evaluating embedded step display name. {0}", ex);
|
|
||||||
}
|
|
||||||
ExecutionContext.Output($"##[start-action display={EscapeProperty(SanitizeDisplayName(step.DisplayName))};id={EscapeProperty(markerId)}]");
|
|
||||||
stepStopwatch = Stopwatch.StartNew();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register Callback
|
// Register Callback
|
||||||
CancellationTokenRegistration? jobCancelRegister = null;
|
CancellationTokenRegistration? jobCancelRegister = null;
|
||||||
try
|
try
|
||||||
@@ -408,14 +381,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
// Condition is false
|
// Condition is false
|
||||||
Trace.Info("Skipping step due to condition evaluation.");
|
Trace.Info("Skipping step due to condition evaluation.");
|
||||||
SetStepConclusion(step, TaskResult.Skipped);
|
SetStepConclusion(step, TaskResult.Skipped);
|
||||||
|
|
||||||
if (emitCompositeMarkers)
|
|
||||||
{
|
|
||||||
stepStopwatch.Stop();
|
|
||||||
ExecutionContext.Output($"##[end-action id={EscapeProperty(markerId)};outcome=skipped;conclusion=skipped;duration_ms=0]");
|
|
||||||
endMarkerEmitted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
else if (conditionEvaluateError != null)
|
else if (conditionEvaluateError != null)
|
||||||
@@ -424,31 +389,13 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
step.ExecutionContext.Error(conditionEvaluateError);
|
step.ExecutionContext.Error(conditionEvaluateError);
|
||||||
SetStepConclusion(step, TaskResult.Failed);
|
SetStepConclusion(step, TaskResult.Failed);
|
||||||
ExecutionContext.Result = TaskResult.Failed;
|
ExecutionContext.Result = TaskResult.Failed;
|
||||||
|
|
||||||
if (emitCompositeMarkers)
|
|
||||||
{
|
|
||||||
stepStopwatch.Stop();
|
|
||||||
ExecutionContext.Output($"##[end-action id={EscapeProperty(markerId)};outcome=failure;conclusion=failure;duration_ms={stepStopwatch.ElapsedMilliseconds}]");
|
|
||||||
endMarkerEmitted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await RunStepAsync(step);
|
await RunStepAsync(step);
|
||||||
|
|
||||||
if (emitCompositeMarkers)
|
|
||||||
{
|
|
||||||
stepStopwatch.Stop();
|
|
||||||
// Outcome = raw result before continue-on-error (null when continue-on-error didn't fire)
|
|
||||||
// Result = final result after continue-on-error
|
|
||||||
var outcome = (step.ExecutionContext.Outcome ?? step.ExecutionContext.Result ?? TaskResult.Succeeded).ToActionResult().ToString().ToLowerInvariant();
|
|
||||||
var conclusion = (step.ExecutionContext.Result ?? TaskResult.Succeeded).ToActionResult().ToString().ToLowerInvariant();
|
|
||||||
ExecutionContext.Output($"##[end-action id={EscapeProperty(markerId)};outcome={outcome};conclusion={conclusion};duration_ms={stepStopwatch.ElapsedMilliseconds}]");
|
|
||||||
endMarkerEmitted = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -457,14 +404,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
jobCancelRegister?.Dispose();
|
jobCancelRegister?.Dispose();
|
||||||
jobCancelRegister = null;
|
jobCancelRegister = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emitCompositeMarkers && !endMarkerEmitted)
|
|
||||||
{
|
|
||||||
stepStopwatch.Stop();
|
|
||||||
var outcome = (step.ExecutionContext.Outcome ?? step.ExecutionContext.Result ?? TaskResult.Failed).ToActionResult().ToString().ToLowerInvariant();
|
|
||||||
var conclusion = (step.ExecutionContext.Result ?? TaskResult.Failed).ToActionResult().ToString().ToLowerInvariant();
|
|
||||||
ExecutionContext.Output($"##[end-action id={EscapeProperty(markerId)};outcome={outcome};conclusion={conclusion};duration_ms={stepStopwatch.ElapsedMilliseconds}]");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Check failed or cancelled
|
// Check failed or cancelled
|
||||||
if (step.ExecutionContext.Result == TaskResult.Failed || step.ExecutionContext.Result == TaskResult.Canceled)
|
if (step.ExecutionContext.Result == TaskResult.Failed || step.ExecutionContext.Result == TaskResult.Canceled)
|
||||||
@@ -531,44 +470,5 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
step.ExecutionContext.Result = result;
|
step.ExecutionContext.Result = result;
|
||||||
step.ExecutionContext.UpdateGlobalStepsContext();
|
step.ExecutionContext.UpdateGlobalStepsContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Escapes marker property values so they cannot break the ##[command key=value] format.
|
|
||||||
/// Delegates to ActionCommand.EscapeValue which escapes `;`, `]`, `\r`, `\n`, and `%`.
|
|
||||||
/// </summary>
|
|
||||||
internal static string EscapeProperty(string value)
|
|
||||||
{
|
|
||||||
return ActionCommand.EscapeValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Maximum character length for display names in markers to prevent log bloat.</summary>
|
|
||||||
internal const int MaxDisplayNameLength = 1000;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes a step display name for safe embedding in a marker property.
|
|
||||||
/// Trims leading whitespace, drops everything after the first newline, and
|
|
||||||
/// truncates to <see cref="MaxDisplayNameLength"/> characters.
|
|
||||||
/// </summary>
|
|
||||||
internal static string SanitizeDisplayName(string displayName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(displayName)) return displayName;
|
|
||||||
|
|
||||||
// Take first line only (FormatStepName in ActionRunner.cs already does this
|
|
||||||
// for most cases, but be defensive for any code path that skips it)
|
|
||||||
var result = displayName.TrimStart(' ', '\t', '\r', '\n');
|
|
||||||
var firstNewLine = result.IndexOfAny(new[] { '\r', '\n' });
|
|
||||||
if (firstNewLine >= 0)
|
|
||||||
{
|
|
||||||
result = result.Substring(0, firstNewLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate excessively long names
|
|
||||||
if (result.Length > MaxDisplayNameLength)
|
|
||||||
{
|
|
||||||
result = result.Substring(0, MaxDisplayNameLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,13 +191,19 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
ArgUtil.Directory(tempWorkflowDirectory, nameof(tempWorkflowDirectory));
|
ArgUtil.Directory(tempWorkflowDirectory, nameof(tempWorkflowDirectory));
|
||||||
|
|
||||||
container.MountVolumes.Add(new MountVolume("/var/run/docker.sock", "/var/run/docker.sock"));
|
container.MountVolumes.Add(new MountVolume("/var/run/docker.sock", "/var/run/docker.sock"));
|
||||||
container.MountVolumes.Add(new MountVolume(tempDirectory, "/github/runner_temp"));
|
if (FeatureManager.IsContainerActionRunnerTempEnabled(ExecutionContext.Global.Variables))
|
||||||
|
{
|
||||||
|
container.MountVolumes.Add(new MountVolume(tempDirectory, "/github/runner_temp"));
|
||||||
|
}
|
||||||
container.MountVolumes.Add(new MountVolume(tempHomeDirectory, "/github/home"));
|
container.MountVolumes.Add(new MountVolume(tempHomeDirectory, "/github/home"));
|
||||||
container.MountVolumes.Add(new MountVolume(tempWorkflowDirectory, "/github/workflow"));
|
container.MountVolumes.Add(new MountVolume(tempWorkflowDirectory, "/github/workflow"));
|
||||||
container.MountVolumes.Add(new MountVolume(tempFileCommandDirectory, "/github/file_commands"));
|
container.MountVolumes.Add(new MountVolume(tempFileCommandDirectory, "/github/file_commands"));
|
||||||
container.MountVolumes.Add(new MountVolume(defaultWorkingDirectory, "/github/workspace"));
|
container.MountVolumes.Add(new MountVolume(defaultWorkingDirectory, "/github/workspace"));
|
||||||
|
|
||||||
container.AddPathTranslateMapping(tempDirectory, "/github/runner_temp");
|
if (FeatureManager.IsContainerActionRunnerTempEnabled(ExecutionContext.Global.Variables))
|
||||||
|
{
|
||||||
|
container.AddPathTranslateMapping(tempDirectory, "/github/runner_temp");
|
||||||
|
}
|
||||||
container.AddPathTranslateMapping(tempHomeDirectory, "/github/home");
|
container.AddPathTranslateMapping(tempHomeDirectory, "/github/home");
|
||||||
container.AddPathTranslateMapping(tempWorkflowDirectory, "/github/workflow");
|
container.AddPathTranslateMapping(tempWorkflowDirectory, "/github/workflow");
|
||||||
container.AddPathTranslateMapping(tempFileCommandDirectory, "/github/file_commands");
|
container.AddPathTranslateMapping(tempFileCommandDirectory, "/github/file_commands");
|
||||||
@@ -239,14 +245,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
Environment["ACTIONS_RESULTS_URL"] = resultsUrl;
|
Environment["ACTIONS_RESULTS_URL"] = resultsUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
|
|
||||||
{
|
|
||||||
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))
|
|
||||||
{
|
|
||||||
Environment["ACTIONS_ORCHESTRATION_ID"] = orchestrationId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var variable in this.Environment)
|
foreach (var variable in this.Environment)
|
||||||
{
|
{
|
||||||
container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value);
|
container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value);
|
||||||
|
|||||||
@@ -25,14 +25,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
|
|
||||||
public sealed class HandlerFactory : RunnerService, IHandlerFactory
|
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(
|
public IHandler Create(
|
||||||
IExecutionContext executionContext,
|
IExecutionContext executionContext,
|
||||||
Pipelines.ActionStepDefinitionReference action,
|
Pipelines.ActionStepDefinitionReference action,
|
||||||
@@ -73,13 +65,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20;
|
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);
|
|
||||||
|
|
||||||
// Check if node20 was explicitly specified in the action
|
// Check if node20 was explicitly specified in the action
|
||||||
// We don't modify if node24 was explicitly specified
|
// We don't modify if node24 was explicitly specified
|
||||||
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
|
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
|
||||||
@@ -88,15 +73,7 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false;
|
bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false;
|
||||||
|
|
||||||
var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24);
|
var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24);
|
||||||
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion, deprecateArm32, killArm32, node20RemovalDate);
|
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion);
|
||||||
|
|
||||||
// ARM32 kill switch: fail the step
|
|
||||||
if (finalNodeVersion == null)
|
|
||||||
{
|
|
||||||
executionContext.Error(platformWarningMessage);
|
|
||||||
throw new InvalidOperationException(platformWarningMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeData.NodeVersion = finalNodeVersion;
|
nodeData.NodeVersion = finalNodeVersion;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(configWarningMessage))
|
if (!string.IsNullOrEmpty(configWarningMessage))
|
||||||
@@ -109,59 +86,14 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
executionContext.Warning(platformWarningMessage);
|
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
|
// Show information about Node 24 migration in Phase 2
|
||||||
if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
|
if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
string infoMessage = "Node 20 is being deprecated. This workflow is running with Node 24 by default. " +
|
string infoMessage = "Node 20 is being deprecated. This workflow is running with Node 24 by default. " +
|
||||||
"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. " +
|
"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable.";
|
||||||
$"For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
|
|
||||||
executionContext.Output(infoMessage);
|
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;
|
(handler as INodeScriptActionHandler).Data = nodeData;
|
||||||
}
|
}
|
||||||
@@ -197,25 +129,5 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
handler.LocalActionContainerSetupSteps = localActionContainerSetupSteps;
|
handler.LocalActionContainerSetupSteps = localActionContainerSetupSteps;
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetActionName(Pipelines.ActionStepDefinitionReference action)
|
|
||||||
{
|
|
||||||
if (action is Pipelines.RepositoryPathReference repoRef)
|
|
||||||
{
|
|
||||||
var pathString = string.Empty;
|
|
||||||
if (!string.IsNullOrEmpty(repoRef.Path))
|
|
||||||
{
|
|
||||||
pathString = string.IsNullOrEmpty(repoRef.Name)
|
|
||||||
? repoRef.Path
|
|
||||||
: $"/{repoRef.Path}";
|
|
||||||
}
|
|
||||||
var repoString = string.IsNullOrEmpty(repoRef.Ref)
|
|
||||||
? $"{repoRef.Name}{pathString}"
|
|
||||||
: $"{repoRef.Name}{pathString}@{repoRef.Ref}";
|
|
||||||
return string.IsNullOrEmpty(repoString) ? null : repoString;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ using GitHub.Runner.Common.Util;
|
|||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.Runner.Worker.Container;
|
||||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||||
using GitHub.Services.Common;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker.Handlers
|
namespace GitHub.Runner.Worker.Handlers
|
||||||
{
|
{
|
||||||
@@ -78,14 +77,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
Environment["ACTIONS_CACHE_SERVICE_V2"] = bool.TrueString;
|
Environment["ACTIONS_CACHE_SERVICE_V2"] = bool.TrueString;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
|
|
||||||
{
|
|
||||||
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))
|
|
||||||
{
|
|
||||||
Environment["ACTIONS_ORCHESTRATION_ID"] = orchestrationId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the target script.
|
// Resolve the target script.
|
||||||
string target = null;
|
string target = null;
|
||||||
if (stage == ActionRunStage.Main)
|
if (stage == ActionRunStage.Main)
|
||||||
@@ -129,15 +120,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
// file name character on Linux.
|
// file name character on Linux.
|
||||||
string arguments = StepHost.ResolvePathForStepHost(ExecutionContext, StringUtil.Format(@"""{0}""", target.Replace(@"""", @"\""")));
|
string arguments = StepHost.ResolvePathForStepHost(ExecutionContext, StringUtil.Format(@"""{0}""", target.Replace(@"""", @"\""")));
|
||||||
|
|
||||||
// Disable maglev jit compiler in node.js 24.x.x on x64 Windows until the node.js bug is fixed.
|
|
||||||
// https://github.com/nodejs/node/issues/62260
|
|
||||||
if (nodeRuntimeVersion.StartsWith("node24", StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
(StringUtil.ConvertToBoolean(System.Environment.GetEnvironmentVariable("ACTIONS_RUNNER_DISABLE_NODE_MAGLEV")) || StringUtil.ConvertToBoolean(Environment.GetValueOrDefault("ACTIONS_RUNNER_DISABLE_NODE_MAGLEV"))))
|
|
||||||
{
|
|
||||||
Trace.Info("Disable maglev jit compiler in node.js");
|
|
||||||
arguments = $"--no-maglev {arguments}";
|
|
||||||
}
|
|
||||||
|
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
// It appears that node.exe outputs UTF8 when not in TTY mode.
|
// It appears that node.exe outputs UTF8 when not in TTY mode.
|
||||||
Encoding outputEncoding = Encoding.UTF8;
|
Encoding outputEncoding = Encoding.UTF8;
|
||||||
|
|||||||
@@ -90,14 +90,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip runner-controlled markers from user output to prevent injection
|
|
||||||
if (!String.IsNullOrEmpty(line) &&
|
|
||||||
(line.Contains("##[start-action") || line.Contains("##[end-action")))
|
|
||||||
{
|
|
||||||
line = line.Replace("##[start-action", @"##[\start-action")
|
|
||||||
.Replace("##[end-action", @"##[\end-action");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Problem matchers
|
// Problem matchers
|
||||||
if (_matchers.Length > 0)
|
if (_matchers.Length > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -318,14 +318,6 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
Environment["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = systemConnection.Authorization.Parameters[EndpointAuthorizationParameters.AccessToken];
|
Environment["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = systemConnection.Authorization.Parameters[EndpointAuthorizationParameters.AccessToken];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
|
|
||||||
{
|
|
||||||
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))
|
|
||||||
{
|
|
||||||
Environment["ACTIONS_ORCHESTRATION_ID"] = orchestrationId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExecutionContext.Debug($"{fileName} {arguments}");
|
ExecutionContext.Debug($"{fileName} {arguments}");
|
||||||
|
|
||||||
Inputs.TryGetValue("standardInInput", out var standardInInput);
|
Inputs.TryGetValue("standardInInput", out var standardInInput);
|
||||||
|
|||||||
@@ -58,23 +58,13 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
|
|
||||||
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
||||||
{
|
{
|
||||||
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
|
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
|
||||||
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
|
var (nodeVersion, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(warningMessage))
|
if (!string.IsNullOrEmpty(warningMessage))
|
||||||
{
|
{
|
||||||
executionContext.Warning(warningMessage);
|
executionContext.Warning(warningMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(nodeVersion);
|
return Task.FromResult(nodeVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,18 +142,8 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
|
|
||||||
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
||||||
{
|
{
|
||||||
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
|
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
|
||||||
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
|
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(warningMessage))
|
if (!string.IsNullOrEmpty(warningMessage))
|
||||||
{
|
{
|
||||||
executionContext.Warning(warningMessage);
|
executionContext.Warning(warningMessage);
|
||||||
@@ -293,18 +273,8 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
|
|
||||||
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
|
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
|
||||||
{
|
{
|
||||||
bool deprecateArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.DeprecateLinuxArm32Flag) ?? false;
|
// Use NodeUtil to check if Node24 is requested but we're on ARM32 Linux
|
||||||
bool killArm32 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.KillLinuxArm32Flag) ?? false;
|
var (nodeExternal, warningMessage) = Common.Util.NodeUtil.CheckNodeVersionForLinuxArm32(preferredVersion);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(warningMessage))
|
if (!string.IsNullOrEmpty(warningMessage))
|
||||||
{
|
{
|
||||||
executionContext.Warning(warningMessage);
|
executionContext.Warning(warningMessage);
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Test")]
|
|
||||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
|
||||||
@@ -82,69 +82,5 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string WorkflowRef
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (this.TryGetValue("workflow_ref", out var value) && value is StringContextData str)
|
|
||||||
{
|
|
||||||
return str.Value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
this["workflow_ref"] = value != null ? new StringContextData(value) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string WorkflowSha
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (this.TryGetValue("workflow_sha", out var value) && value is StringContextData str)
|
|
||||||
{
|
|
||||||
return str.Value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
this["workflow_sha"] = value != null ? new StringContextData(value) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string WorkflowRepository
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (this.TryGetValue("workflow_repository", out var value) && value is StringContextData str)
|
|
||||||
{
|
|
||||||
return str.Value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
this["workflow_repository"] = value != null ? new StringContextData(value) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string WorkflowFilePath
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (this.TryGetValue("workflow_file_path", out var value) && value is StringContextData str)
|
|
||||||
{
|
|
||||||
return str.Value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
this["workflow_file_path"] = value != null ? new StringContextData(value) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ using GitHub.DistributedTask.WebApi;
|
|||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Dap;
|
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
@@ -51,7 +50,6 @@ namespace GitHub.Runner.Worker
|
|||||||
private Task _diskSpaceCheckTask = null;
|
private Task _diskSpaceCheckTask = null;
|
||||||
private CancellationTokenSource _serviceConnectivityCheckToken = new();
|
private CancellationTokenSource _serviceConnectivityCheckToken = new();
|
||||||
private Task _serviceConnectivityCheckTask = null;
|
private Task _serviceConnectivityCheckTask = null;
|
||||||
private IDapDebugger _dapDebugger;
|
|
||||||
|
|
||||||
// Download all required actions.
|
// Download all required actions.
|
||||||
// Make sure all condition inputs are valid.
|
// Make sure all condition inputs are valid.
|
||||||
@@ -69,7 +67,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
List<IStep> preJobSteps = new();
|
List<IStep> preJobSteps = new();
|
||||||
List<IStep> jobSteps = new();
|
List<IStep> jobSteps = new();
|
||||||
var initSucceeded = false;
|
|
||||||
using (var register = jobContext.CancellationToken.Register(() => { context.CancelToken(); }))
|
using (var register = jobContext.CancellationToken.Register(() => { context.CancelToken(); }))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -80,25 +77,20 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
var setting = HostContext.GetService<IConfigurationStore>().GetSettings();
|
var setting = HostContext.GetService<IConfigurationStore>().GetSettings();
|
||||||
var credFile = HostContext.GetConfigFile(WellKnownConfigFile.Credentials);
|
var credFile = HostContext.GetConfigFile(WellKnownConfigFile.Credentials);
|
||||||
var credData = File.Exists(credFile) ? IOUtil.LoadObject<CredentialData>(credFile) : null;
|
if (File.Exists(credFile))
|
||||||
// self-hosted runner is the only runner type using OAuth, can be identified via clientId
|
|
||||||
if (credData != null &&
|
|
||||||
credData.Data.TryGetValue("clientId", out _))
|
|
||||||
{
|
{
|
||||||
context.Output($"Runner name: '{setting.AgentName}'");
|
var credData = IOUtil.LoadObject<CredentialData>(credFile);
|
||||||
// use system variable for group name since self-hosted runners can be renamed
|
if (credData != null &&
|
||||||
if (message.Variables.TryGetValue("system.runnerGroupName", out VariableValue runnerGroupName))
|
credData.Data.TryGetValue("clientId", out var clientId))
|
||||||
{
|
{
|
||||||
context.Output($"Runner group name: '{runnerGroupName.Value}'");
|
// print out HostName for self-hosted runner
|
||||||
|
context.Output($"Runner name: '{setting.AgentName}'");
|
||||||
|
if (message.Variables.TryGetValue("system.runnerGroupName", out VariableValue runnerGroupName))
|
||||||
|
{
|
||||||
|
context.Output($"Runner group name: '{runnerGroupName.Value}'");
|
||||||
|
}
|
||||||
|
context.Output($"Machine name: '{Environment.MachineName}'");
|
||||||
}
|
}
|
||||||
// print out machine name for self-hosted runner
|
|
||||||
context.Output($"Machine name: '{Environment.MachineName}'");
|
|
||||||
}
|
|
||||||
// print runner info for lhr runners, skips standard runners (PoolId = 0)
|
|
||||||
else if (setting.PoolId > 0 && !string.IsNullOrEmpty(setting.PoolName) && !string.IsNullOrEmpty(setting.AgentName))
|
|
||||||
{
|
|
||||||
context.Output($"Runner name: '{setting.AgentName}'");
|
|
||||||
context.Output($"Runner group name: '{setting.PoolName}'");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupInfoFile = HostContext.GetConfigFile(WellKnownConfigFile.SetupInfo);
|
var setupInfoFile = HostContext.GetConfigFile(WellKnownConfigFile.SetupInfo);
|
||||||
@@ -120,13 +112,6 @@ namespace GitHub.Runner.Worker
|
|||||||
groupName = "Machine Setup Info";
|
groupName = "Machine Setup Info";
|
||||||
}
|
}
|
||||||
|
|
||||||
// not output internal groups
|
|
||||||
if (groupName.StartsWith("_internal_", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
jobContext.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.General, Message = info.Detail });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Output($"##[group]{groupName}");
|
context.Output($"##[group]{groupName}");
|
||||||
var multiLines = info.Detail.Replace("\r\n", "\n").TrimEnd('\n').Split('\n');
|
var multiLines = info.Detail.Replace("\r\n", "\n").TrimEnd('\n').Split('\n');
|
||||||
foreach (var line in multiLines)
|
foreach (var line in multiLines)
|
||||||
@@ -345,38 +330,6 @@ namespace GitHub.Runner.Worker
|
|||||||
preJobSteps.Add(preStep);
|
preJobSteps.Add(preStep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (step.Type == Pipelines.StepType.BackgroundStepControl)
|
|
||||||
{
|
|
||||||
var ctrl = step as Pipelines.BackgroundStepControl;
|
|
||||||
Trace.Info($"Adding {ctrl.ControlType} step for: {string.Join(", ", ctrl.StepIds ?? Array.Empty<string>())}");
|
|
||||||
var controlType = ctrl.ControlType;
|
|
||||||
if (string.IsNullOrEmpty(controlType))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Background step control '{step.Name}' has no control type.");
|
|
||||||
}
|
|
||||||
if (controlType != Pipelines.BackgroundControlTypes.Wait &&
|
|
||||||
controlType != Pipelines.BackgroundControlTypes.WaitAll &&
|
|
||||||
controlType != Pipelines.BackgroundControlTypes.Cancel)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Unknown background step control type '{controlType}' for step '{step.Name}'.");
|
|
||||||
}
|
|
||||||
var displayName = (ctrl.DisplayNameToken as GitHub.DistributedTask.ObjectTemplating.Tokens.StringToken)?.Value
|
|
||||||
?? step.DisplayName ?? step.Name ?? ctrl.ControlType;
|
|
||||||
var data = new BackgroundStepControlFlowData
|
|
||||||
{
|
|
||||||
Type = controlType,
|
|
||||||
StepId = step.Id,
|
|
||||||
StepName = step.Name,
|
|
||||||
StepIds = ctrl.StepIds,
|
|
||||||
ParallelGroupId = ctrl.ParallelGroupId,
|
|
||||||
};
|
|
||||||
var bgCoord = HostContext.GetService<IBackgroundStepCoordinator>();
|
|
||||||
jobSteps.Add(new JobExtensionRunner(
|
|
||||||
runAsync: bgCoord.RunControlFlowAsync,
|
|
||||||
condition: $"{PipelineTemplateConstants.Always}()",
|
|
||||||
displayName: displayName,
|
|
||||||
data: data));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.Variables.TryGetValue("system.workflowFileFullPath", out VariableValue workflowFileFullPath))
|
if (message.Variables.TryGetValue("system.workflowFileFullPath", out VariableValue workflowFileFullPath))
|
||||||
@@ -432,107 +385,13 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create execution context for job steps
|
// Create execution context for job steps
|
||||||
// Build mapping of logical step ID (ContextName) → external ID (timeline record GUID)
|
|
||||||
// so wait/cancel steps can reference background steps by external ID.
|
|
||||||
var contextNameToExternalId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
var hasBackgroundSteps = false;
|
|
||||||
var backgroundStepExternalIds = new List<string>();
|
|
||||||
|
|
||||||
// Track which background steps are explicitly covered by wait/wait-all/cancel
|
|
||||||
var coveredBackgroundIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var step in jobSteps)
|
foreach (var step in jobSteps)
|
||||||
{
|
{
|
||||||
if (step is IActionRunner actionStep)
|
if (step is IActionRunner actionStep)
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(actionStep, step.DisplayName);
|
ArgUtil.NotNull(actionStep, step.DisplayName);
|
||||||
intraActionStates.TryGetValue(actionStep.Action.Id, out var intraActionState);
|
intraActionStates.TryGetValue(actionStep.Action.Id, out var intraActionState);
|
||||||
|
actionStep.ExecutionContext = jobContext.CreateChild(actionStep.Action.Id, actionStep.DisplayName, actionStep.Action.Name, null, actionStep.Action.ContextName, ActionRunStage.Main, intraActionState);
|
||||||
var isBg = actionStep.Action?.Background == true;
|
|
||||||
actionStep.ExecutionContext = jobContext.CreateChild(
|
|
||||||
actionStep.Action.Id, actionStep.DisplayName, actionStep.Action.Name,
|
|
||||||
null, actionStep.Action.ContextName, ActionRunStage.Main, intraActionState,
|
|
||||||
isBackground: isBg,
|
|
||||||
parallelGroupId: isBg ? actionStep.Action.ParallelGroupId : null);
|
|
||||||
|
|
||||||
if (isBg)
|
|
||||||
{
|
|
||||||
hasBackgroundSteps = true;
|
|
||||||
var externalId = actionStep.Action.Id.ToString("N");
|
|
||||||
contextNameToExternalId[actionStep.Action.ContextName] = externalId;
|
|
||||||
backgroundStepExternalIds.Add(externalId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (step is JobExtensionRunner runnerStep && runnerStep.Data is BackgroundStepControlFlowData cf)
|
|
||||||
{
|
|
||||||
// Resolve step IDs to external IDs and track coverage
|
|
||||||
string[] externalIds = null;
|
|
||||||
if (cf.StepIds != null && cf.StepIds.Length > 0)
|
|
||||||
{
|
|
||||||
foreach (var id in cf.StepIds)
|
|
||||||
{
|
|
||||||
coveredBackgroundIds.Add(id);
|
|
||||||
}
|
|
||||||
externalIds = cf.StepIds
|
|
||||||
.Where(id => contextNameToExternalId.ContainsKey(id))
|
|
||||||
.Select(id => contextNameToExternalId[id])
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cf.Type == Pipelines.BackgroundControlTypes.WaitAll)
|
|
||||||
{
|
|
||||||
externalIds = backgroundStepExternalIds.Count > 0 ? backgroundStepExternalIds.ToArray() : null;
|
|
||||||
foreach (var id in contextNameToExternalId.Keys)
|
|
||||||
{
|
|
||||||
coveredBackgroundIds.Add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
step.ExecutionContext = jobContext.CreateChild(
|
|
||||||
cf.StepId, step.DisplayName, cf.StepName,
|
|
||||||
null, cf.StepName, ActionRunStage.Main,
|
|
||||||
backgroundControlType: cf.Type,
|
|
||||||
backgroundControlStepIds: externalIds,
|
|
||||||
parallelGroupId: cf.ParallelGroupId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add implicit wait-all only if there are background steps not covered by any wait/wait-all/cancel
|
|
||||||
var allBackgroundIds = contextNameToExternalId.Keys;
|
|
||||||
var hasUncoveredBackgroundSteps = allBackgroundIds.Any(id => !coveredBackgroundIds.Contains(id));
|
|
||||||
if (hasBackgroundSteps)
|
|
||||||
{
|
|
||||||
// Initialize coordinator only when there are background steps
|
|
||||||
var bgCoordinator = HostContext.GetService<IBackgroundStepCoordinator>();
|
|
||||||
var maxBgSteps = jobContext.Global.Variables.GetInt("system.runner.maxbackgroundsteps");
|
|
||||||
var maxConcurrent = (maxBgSteps.HasValue && maxBgSteps.Value > 0) ? maxBgSteps.Value : 10;
|
|
||||||
bgCoordinator.InitializeCoordinator(maxConcurrent);
|
|
||||||
|
|
||||||
// Add implicit wait-all only if there are uncovered background steps
|
|
||||||
if (hasUncoveredBackgroundSteps)
|
|
||||||
{
|
|
||||||
var implicitStepId = Guid.NewGuid();
|
|
||||||
var implicitWaitAllData = new BackgroundStepControlFlowData
|
|
||||||
{
|
|
||||||
Type = Pipelines.BackgroundControlTypes.WaitAll,
|
|
||||||
StepId = implicitStepId,
|
|
||||||
StepName = "__implicit_wait_all",
|
|
||||||
};
|
|
||||||
var implicitWaitAll = new JobExtensionRunner(
|
|
||||||
runAsync: bgCoordinator.RunControlFlowAsync,
|
|
||||||
condition: $"{PipelineTemplateConstants.Always}()",
|
|
||||||
displayName: "Wait for all background steps",
|
|
||||||
data: implicitWaitAllData);
|
|
||||||
var uncoveredExternalIds = contextNameToExternalId
|
|
||||||
.Where(kvp => !coveredBackgroundIds.Contains(kvp.Key))
|
|
||||||
.Select(kvp => kvp.Value)
|
|
||||||
.ToArray();
|
|
||||||
implicitWaitAll.ExecutionContext = jobContext.CreateChild(
|
|
||||||
implicitStepId, implicitWaitAll.DisplayName, "__implicit_wait_all",
|
|
||||||
null, "__implicit_wait_all", ActionRunStage.Main,
|
|
||||||
backgroundControlType: Pipelines.BackgroundControlTypes.WaitAll,
|
|
||||||
backgroundControlStepIds: uncoveredExternalIds.Length > 0 ? uncoveredExternalIds : null);
|
|
||||||
jobSteps.Add(implicitWaitAll);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,41 +469,6 @@ namespace GitHub.Runner.Worker
|
|||||||
Trace.Info($"Start checking service connectivity in background.");
|
Trace.Info($"Start checking service connectivity in background.");
|
||||||
_serviceConnectivityCheckTask = CheckServiceConnectivityAsync(context, _serviceConnectivityCheckToken.Token);
|
_serviceConnectivityCheckTask = CheckServiceConnectivityAsync(context, _serviceConnectivityCheckToken.Token);
|
||||||
|
|
||||||
// Start the DAP debugger and wait for a client connection inside
|
|
||||||
// "Set up job" so the step stays in-progress while we wait.
|
|
||||||
if (jobContext.Global.Debugger?.Enabled == true)
|
|
||||||
{
|
|
||||||
Trace.Info("Debugger enabled — starting inside Set up job");
|
|
||||||
context.Output("Starting debugger…");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_dapDebugger = HostContext.GetService<IDapDebugger>();
|
|
||||||
await _dapDebugger.StartAsync(jobContext);
|
|
||||||
|
|
||||||
context.Output("Waiting for debugger client to connect…");
|
|
||||||
|
|
||||||
await _dapDebugger.WaitUntilReadyAsync();
|
|
||||||
context.Output("Debugger connected.");
|
|
||||||
AddDebuggerConnectionTelemetry(jobContext, "Connected");
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (jobContext.CancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
Trace.Info("Job was cancelled before debugger client connected.");
|
|
||||||
AddDebuggerConnectionTelemetry(jobContext, "Canceled");
|
|
||||||
context.Error("Job was cancelled before debugger client connected.");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Error($"DAP debugger failed: {ex.Message}");
|
|
||||||
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.GetType().Name}");
|
|
||||||
context.Error("The debugger failed to start or no debugger client connected in time.");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initSucceeded = true;
|
|
||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
|
||||||
@@ -665,36 +489,12 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// If InitializeJob failed after the debugger was started,
|
|
||||||
// tear down the transport here since FinalizeJob won't run.
|
|
||||||
if (!initSucceeded && _dapDebugger != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _dapDebugger.StopAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Warning($"DAP debugger cleanup during failed init: {ex.Message}");
|
|
||||||
}
|
|
||||||
_dapDebugger = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Debug("Finishing: Set up job");
|
context.Debug("Finishing: Set up job");
|
||||||
context.Complete();
|
context.Complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddDebuggerConnectionTelemetry(IExecutionContext jobContext, string result)
|
|
||||||
{
|
|
||||||
jobContext.Global.JobTelemetry.Add(new JobTelemetry
|
|
||||||
{
|
|
||||||
Type = JobTelemetryType.General,
|
|
||||||
Message = $"DebuggerConnectionResult: {result}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetWorkflowReference(IDictionary<string, VariableValue> variables)
|
private string GetWorkflowReference(IDictionary<string, VariableValue> variables)
|
||||||
{
|
{
|
||||||
var reference = "";
|
var reference = "";
|
||||||
@@ -928,39 +728,6 @@ namespace GitHub.Runner.Worker
|
|||||||
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = $"Fail to check service connectivity. {ex.Message}" });
|
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = $"Fail to check service connectivity. {ex.Message}" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
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}";
|
|
||||||
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -970,34 +737,6 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
// Pause for debugger inspection, then tear down the DAP session.
|
|
||||||
// OnJobCompletedAsync pauses first, then sends terminated/exited
|
|
||||||
// events and stops the transport.
|
|
||||||
if (_dapDebugger != null)
|
|
||||||
{
|
|
||||||
context.Output("Job completed — pausing for debugger inspection. Press continue to finish.");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _dapDebugger.OnJobCompletedAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Warning($"DAP debugger completion error: {ex.Message}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _dapDebugger.StopAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Warning($"DAP debugger stop error: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_dapDebugger = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Debug("Finishing: Complete job");
|
context.Debug("Finishing: Complete job");
|
||||||
context.Complete();
|
context.Complete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using GitHub.DistributedTask.WebApi;
|
|||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Dap;
|
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
using Sdk.RSWebApi.Contracts;
|
using Sdk.RSWebApi.Contracts;
|
||||||
@@ -179,7 +178,6 @@ namespace GitHub.Runner.Worker
|
|||||||
_tempDirectoryManager = HostContext.GetService<ITempDirectoryManager>();
|
_tempDirectoryManager = HostContext.GetService<ITempDirectoryManager>();
|
||||||
_tempDirectoryManager.InitializeTempDirectory(jobContext);
|
_tempDirectoryManager.InitializeTempDirectory(jobContext);
|
||||||
|
|
||||||
|
|
||||||
// Get the job extension.
|
// Get the job extension.
|
||||||
Trace.Info("Getting job extension.");
|
Trace.Info("Getting job extension.");
|
||||||
IJobExtension jobExtension = HostContext.CreateService<IJobExtension>();
|
IJobExtension jobExtension = HostContext.CreateService<IJobExtension>();
|
||||||
@@ -231,12 +229,6 @@ namespace GitHub.Runner.Worker
|
|||||||
jobContext.JobSteps.Enqueue(step);
|
jobContext.JobSteps.Enqueue(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (jobContext.Global.Debugger?.Enabled == true)
|
|
||||||
{
|
|
||||||
var dapDebugger = HostContext.GetService<IDapDebugger>();
|
|
||||||
await dapDebugger.OnJobStepsInitializedAsync(jobContext.JobSteps, jobContext.PostJobSteps);
|
|
||||||
}
|
|
||||||
|
|
||||||
await stepsRunner.RunAsync(jobContext);
|
await stepsRunner.RunAsync(jobContext);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
|
||||||
using GitHub.Actions.WorkflowParser;
|
using GitHub.Actions.WorkflowParser;
|
||||||
using GitHub.DistributedTask.Expressions2;
|
using GitHub.DistributedTask.Expressions2;
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
@@ -24,7 +23,6 @@ namespace GitHub.Runner.Worker
|
|||||||
public PipelineTemplateEvaluatorWrapper(
|
public PipelineTemplateEvaluatorWrapper(
|
||||||
IHostContext hostContext,
|
IHostContext hostContext,
|
||||||
IExecutionContext context,
|
IExecutionContext context,
|
||||||
bool allowServiceContainerCommand,
|
|
||||||
ObjectTemplating.ITraceWriter traceWriter = null)
|
ObjectTemplating.ITraceWriter traceWriter = null)
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(hostContext, nameof(hostContext));
|
ArgUtil.NotNull(hostContext, nameof(hostContext));
|
||||||
@@ -42,14 +40,11 @@ namespace GitHub.Runner.Worker
|
|||||||
_legacyEvaluator = new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable)
|
_legacyEvaluator = new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable)
|
||||||
{
|
{
|
||||||
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
|
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
|
||||||
AllowServiceContainerCommand = allowServiceContainerCommand,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// New evaluator
|
// New evaluator
|
||||||
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
|
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
|
||||||
var features = WorkflowFeatures.GetDefaults();
|
_newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, context.Global.FileTable, features: null)
|
||||||
features.AllowServiceContainerCommand = allowServiceContainerCommand;
|
|
||||||
_newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, context.Global.FileTable, features)
|
|
||||||
{
|
{
|
||||||
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
|
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
|
||||||
};
|
};
|
||||||
@@ -221,19 +216,12 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal TLegacy EvaluateAndCompare<TLegacy, TNew>(
|
private TLegacy EvaluateAndCompare<TLegacy, TNew>(
|
||||||
string methodName,
|
string methodName,
|
||||||
Func<TLegacy> legacyEvaluator,
|
Func<TLegacy> legacyEvaluator,
|
||||||
Func<TNew> newEvaluator,
|
Func<TNew> newEvaluator,
|
||||||
Func<TLegacy, TNew, bool> resultComparer)
|
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;
|
|
||||||
|
|
||||||
// Legacy evaluator
|
// Legacy evaluator
|
||||||
var legacyException = default(Exception);
|
var legacyException = default(Exception);
|
||||||
var legacyResult = default(TLegacy);
|
var legacyResult = default(TLegacy);
|
||||||
@@ -265,18 +253,14 @@ namespace GitHub.Runner.Worker
|
|||||||
newException = ex;
|
newException = ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture cancellation state after evaluation
|
|
||||||
var cancellationRequestedAfter = rootCancellationToken.IsCancellationRequested;
|
|
||||||
|
|
||||||
// Compare results or exceptions
|
// Compare results or exceptions
|
||||||
bool hasMismatch = false;
|
|
||||||
if (legacyException != null || newException != null)
|
if (legacyException != null || newException != null)
|
||||||
{
|
{
|
||||||
// Either one or both threw exceptions - compare them
|
// Either one or both threw exceptions - compare them
|
||||||
if (!CompareExceptions(legacyException, newException))
|
if (!CompareExceptions(legacyException, newException))
|
||||||
{
|
{
|
||||||
_trace.Info($"{methodName} exception mismatch");
|
_trace.Info($"{methodName} exception mismatch");
|
||||||
hasMismatch = true;
|
RecordMismatch($"{methodName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -285,20 +269,6 @@ namespace GitHub.Runner.Worker
|
|||||||
if (!resultComparer(legacyResult, newResult))
|
if (!resultComparer(legacyResult, newResult))
|
||||||
{
|
{
|
||||||
_trace.Info($"{methodName} mismatch");
|
_trace.Info($"{methodName} mismatch");
|
||||||
hasMismatch = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only record mismatch if it wasn't caused by a cancellation race condition
|
|
||||||
if (hasMismatch)
|
|
||||||
{
|
|
||||||
if (!cancellationRequestedBefore && cancellationRequestedAfter)
|
|
||||||
{
|
|
||||||
// Cancellation state changed during evaluation window - skip recording
|
|
||||||
_trace.Info($"{methodName} mismatch skipped due to cancellation race condition");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RecordMismatch($"{methodName}");
|
RecordMismatch($"{methodName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,18 +380,6 @@ namespace GitHub.Runner.Worker
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(legacyResult.Entrypoint, newResult.Entrypoint, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobContainer mismatch - Entrypoint differs (legacy='{legacyResult.Entrypoint}', new='{newResult.Entrypoint}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(legacyResult.Command, newResult.Command, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobContainer mismatch - Command differs (legacy='{legacyResult.Command}', new='{newResult.Command}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CompareDictionaries(legacyResult.Environment, newResult.Environment, "Environment"))
|
if (!CompareDictionaries(legacyResult.Environment, newResult.Environment, "Environment"))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -654,13 +612,6 @@ namespace GitHub.Runner.Worker
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for known equivalent error patterns (e.g., JSON parse errors)
|
|
||||||
// where both parsers correctly reject invalid input but with different wording
|
|
||||||
if (IsKnownEquivalentErrorPattern(legacyException, newException))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare exception messages recursively (including inner exceptions)
|
// Compare exception messages recursively (including inner exceptions)
|
||||||
var legacyMessages = GetExceptionMessages(legacyException);
|
var legacyMessages = GetExceptionMessages(legacyException);
|
||||||
var newMessages = GetExceptionMessages(newException);
|
var newMessages = GetExceptionMessages(newException);
|
||||||
@@ -683,67 +634,6 @@ namespace GitHub.Runner.Worker
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if two exceptions match a known pattern where both parsers correctly reject
|
|
||||||
/// invalid input but with different error messages (e.g., JSON parse errors from fromJSON).
|
|
||||||
/// </summary>
|
|
||||||
private bool IsKnownEquivalentErrorPattern(Exception legacyException, Exception newException)
|
|
||||||
{
|
|
||||||
// fromJSON('') - both parsers fail when parsing empty string as JSON
|
|
||||||
// The error messages differ but both indicate JSON parsing failure.
|
|
||||||
// Legacy throws raw JsonReaderException: "Error reading JToken from JsonReader..."
|
|
||||||
// New wraps it: "Error parsing fromJson" with inner JsonReaderException
|
|
||||||
// Both may be wrapped in TemplateValidationException: "The template is not valid..."
|
|
||||||
if (HasJsonExceptionType(legacyException) && HasJsonExceptionType(newException))
|
|
||||||
{
|
|
||||||
_trace.Info("CompareExceptions - both exceptions are JSON parse errors, treating as matched");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the exception chain contains a JSON-related exception type.
|
|
||||||
/// </summary>
|
|
||||||
internal static bool HasJsonExceptionType(Exception ex)
|
|
||||||
{
|
|
||||||
var toProcess = new Queue<Exception>();
|
|
||||||
toProcess.Enqueue(ex);
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
while (toProcess.Count > 0 && count < 50)
|
|
||||||
{
|
|
||||||
var current = toProcess.Dequeue();
|
|
||||||
if (current == null) continue;
|
|
||||||
|
|
||||||
count++;
|
|
||||||
|
|
||||||
if (current is Newtonsoft.Json.JsonReaderException ||
|
|
||||||
current is System.Text.Json.JsonException)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current is AggregateException aggregateEx)
|
|
||||||
{
|
|
||||||
foreach (var innerEx in aggregateEx.InnerExceptions)
|
|
||||||
{
|
|
||||||
if (innerEx != null && count < 50)
|
|
||||||
{
|
|
||||||
toProcess.Enqueue(innerEx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (current.InnerException != null)
|
|
||||||
{
|
|
||||||
toProcess.Enqueue(current.InnerException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IList<string> GetExceptionMessages(Exception ex)
|
private IList<string> GetExceptionMessages(Exception ex)
|
||||||
{
|
{
|
||||||
var messages = new List<string>();
|
var messages = new List<string>();
|
||||||
|
|||||||
@@ -19,11 +19,10 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
|
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
|
||||||
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
|
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
|
||||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||||
<PackageReference Include="Microsoft.DevTunnels.Connections" Version="1.3.48" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
private static readonly Regex _propertyRegex = new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled);
|
private static readonly Regex _propertyRegex = new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled);
|
||||||
private readonly DictionaryContextData _contextData = new();
|
private readonly DictionaryContextData _contextData = new();
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears memory for a composite action's isolated "steps" context, after the action
|
/// Clears memory for a composite action's isolated "steps" context, after the action
|
||||||
@@ -26,12 +25,9 @@ namespace GitHub.Runner.Worker
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void ClearScope(string scopeName)
|
public void ClearScope(string scopeName)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
if (_contextData.TryGetValue(scopeName, out _))
|
||||||
{
|
{
|
||||||
if (_contextData.TryGetValue(scopeName, out _))
|
_contextData[scopeName] = new DictionaryContextData();
|
||||||
{
|
|
||||||
_contextData[scopeName] = new DictionaryContextData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,26 +41,23 @@ namespace GitHub.Runner.Worker
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DictionaryContextData GetScope(string scopeName)
|
public DictionaryContextData GetScope(string scopeName)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
if (scopeName == null)
|
||||||
{
|
{
|
||||||
if (scopeName == null)
|
scopeName = string.Empty;
|
||||||
{
|
|
||||||
scopeName = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var scope = default(DictionaryContextData);
|
|
||||||
if (_contextData.TryGetValue(scopeName, out var scopeValue))
|
|
||||||
{
|
|
||||||
scope = scopeValue.AssertDictionary("scope");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
scope = new DictionaryContextData();
|
|
||||||
_contextData.Add(scopeName, scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
return scope;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scope = default(DictionaryContextData);
|
||||||
|
if (_contextData.TryGetValue(scopeName, out var scopeValue))
|
||||||
|
{
|
||||||
|
scope = scopeValue.AssertDictionary("scope");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
scope = new DictionaryContextData();
|
||||||
|
_contextData.Add(scopeName, scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetOutput(
|
public void SetOutput(
|
||||||
@@ -74,19 +67,16 @@ namespace GitHub.Runner.Worker
|
|||||||
string value,
|
string value,
|
||||||
out string reference)
|
out string reference)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
var step = GetStep(scopeName, stepName);
|
||||||
|
var outputs = step["outputs"].AssertDictionary("outputs");
|
||||||
|
outputs[outputName] = new StringContextData(value);
|
||||||
|
if (_propertyRegex.IsMatch(outputName))
|
||||||
{
|
{
|
||||||
var step = GetStep(scopeName, stepName);
|
reference = $"steps.{stepName}.outputs.{outputName}";
|
||||||
var outputs = step["outputs"].AssertDictionary("outputs");
|
}
|
||||||
outputs[outputName] = new StringContextData(value);
|
else
|
||||||
if (_propertyRegex.IsMatch(outputName))
|
{
|
||||||
{
|
reference = $"steps['{stepName}']['outputs']['{outputName}']";
|
||||||
reference = $"steps.{stepName}.outputs.{outputName}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reference = $"steps['{stepName}']['outputs']['{outputName}']";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +85,8 @@ namespace GitHub.Runner.Worker
|
|||||||
string stepName,
|
string stepName,
|
||||||
ActionResult conclusion)
|
ActionResult conclusion)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
var step = GetStep(scopeName, stepName);
|
||||||
{
|
step["conclusion"] = new StringContextData(conclusion.ToString().ToLowerInvariant());
|
||||||
var step = GetStep(scopeName, stepName);
|
|
||||||
step["conclusion"] = new StringContextData(conclusion.ToString().ToLowerInvariant());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetOutcome(
|
public void SetOutcome(
|
||||||
@@ -107,11 +94,8 @@ namespace GitHub.Runner.Worker
|
|||||||
string stepName,
|
string stepName,
|
||||||
ActionResult outcome)
|
ActionResult outcome)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
var step = GetStep(scopeName, stepName);
|
||||||
{
|
step["outcome"] = new StringContextData(outcome.ToString().ToLowerInvariant());
|
||||||
var step = GetStep(scopeName, stepName);
|
|
||||||
step["outcome"] = new StringContextData(outcome.ToString().ToLowerInvariant());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DictionaryContextData GetStep(string scopeName, string stepName)
|
private DictionaryContextData GetStep(string scopeName, string stepName)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using GitHub.DistributedTask.WebApi;
|
|||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Dap;
|
|
||||||
using GitHub.Runner.Worker.Expressions;
|
using GitHub.Runner.Worker.Expressions;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
namespace GitHub.Runner.Worker
|
||||||
@@ -41,8 +40,6 @@ namespace GitHub.Runner.Worker
|
|||||||
ArgUtil.NotNull(jobContext, nameof(jobContext));
|
ArgUtil.NotNull(jobContext, nameof(jobContext));
|
||||||
ArgUtil.NotNull(jobContext.JobSteps, nameof(jobContext.JobSteps));
|
ArgUtil.NotNull(jobContext.JobSteps, nameof(jobContext.JobSteps));
|
||||||
|
|
||||||
var _bgCoordinator = HostContext.GetService<IBackgroundStepCoordinator>();
|
|
||||||
|
|
||||||
// TaskResult:
|
// TaskResult:
|
||||||
// Abandoned (Server set this.)
|
// Abandoned (Server set this.)
|
||||||
// Canceled
|
// Canceled
|
||||||
@@ -53,21 +50,11 @@ namespace GitHub.Runner.Worker
|
|||||||
jobContext.JobContext.Status = (jobContext.Result ?? TaskResult.Succeeded).ToActionResult();
|
jobContext.JobContext.Status = (jobContext.Result ?? TaskResult.Succeeded).ToActionResult();
|
||||||
var scopeInputs = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
var scopeInputs = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||||
bool checkPostJobActions = false;
|
bool checkPostJobActions = false;
|
||||||
var dapDebugger = HostContext.GetService<IDapDebugger>();
|
|
||||||
while (jobContext.JobSteps.Count > 0 || !checkPostJobActions)
|
while (jobContext.JobSteps.Count > 0 || !checkPostJobActions)
|
||||||
{
|
{
|
||||||
if (jobContext.JobSteps.Count == 0 && !checkPostJobActions)
|
if (jobContext.JobSteps.Count == 0 && !checkPostJobActions)
|
||||||
{
|
{
|
||||||
checkPostJobActions = true;
|
checkPostJobActions = true;
|
||||||
|
|
||||||
// Safety net: wait for any unwaited background steps before post-hooks
|
|
||||||
var backgroundResult = await _bgCoordinator.WaitForUnwaitedStepsAsync(jobContext.CancellationToken);
|
|
||||||
if (backgroundResult != TaskResult.Succeeded)
|
|
||||||
{
|
|
||||||
jobContext.Result = TaskResultUtil.MergeTaskResults(jobContext.Result, backgroundResult);
|
|
||||||
jobContext.JobContext.Status = jobContext.Result?.ToActionResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
while (jobContext.PostJobSteps.TryPop(out var postStep))
|
while (jobContext.PostJobSteps.TryPop(out var postStep))
|
||||||
{
|
{
|
||||||
jobContext.JobSteps.Enqueue(postStep);
|
jobContext.JobSteps.Enqueue(postStep);
|
||||||
@@ -83,11 +70,8 @@ namespace GitHub.Runner.Worker
|
|||||||
ArgUtil.NotNull(step.ExecutionContext.Global, nameof(step.ExecutionContext.Global));
|
ArgUtil.NotNull(step.ExecutionContext.Global, nameof(step.ExecutionContext.Global));
|
||||||
ArgUtil.NotNull(step.ExecutionContext.Global.Variables, nameof(step.ExecutionContext.Global.Variables));
|
ArgUtil.NotNull(step.ExecutionContext.Global.Variables, nameof(step.ExecutionContext.Global.Variables));
|
||||||
|
|
||||||
// Start — defer for background steps until the slot is acquired
|
// Start
|
||||||
if (!step.ExecutionContext.IsBackground)
|
step.ExecutionContext.Start();
|
||||||
{
|
|
||||||
step.ExecutionContext.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expression functions
|
// Expression functions
|
||||||
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<AlwaysFunction>(PipelineTemplateConstants.Always, 0, 0));
|
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<AlwaysFunction>(PipelineTemplateConstants.Always, 0, 0));
|
||||||
@@ -242,22 +226,9 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (step.ExecutionContext.IsBackground)
|
// Run the step
|
||||||
{
|
await RunStepAsync(step, jobContext.CancellationToken);
|
||||||
// Queue the background step via coordinator
|
CompleteStep(step);
|
||||||
_bgCoordinator.StartBackgroundStep(step, jobContext.CancellationToken);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Pause for DAP debugger before step execution
|
|
||||||
await dapDebugger?.OnStepStartingAsync(step);
|
|
||||||
|
|
||||||
// Run the step synchronously (normal behavior)
|
|
||||||
await RunStepAsync(step, jobContext.CancellationToken);
|
|
||||||
CompleteStep(step);
|
|
||||||
|
|
||||||
dapDebugger?.OnStepCompleted(step);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -284,7 +255,6 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
Trace.Info($"Current state: job state = '{jobContext.Result}'");
|
Trace.Info($"Current state: job state = '{jobContext.Result}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunStepAsync(IStep step, CancellationToken jobCancellationToken)
|
private async Task RunStepAsync(IStep step, CancellationToken jobCancellationToken)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ namespace GitHub.DistributedTask.Expressions2
|
|||||||
{
|
{
|
||||||
static ExpressionConstants()
|
static ExpressionConstants()
|
||||||
{
|
{
|
||||||
AddFunction<Case>("case", 3, Byte.MaxValue);
|
|
||||||
AddFunction<Contains>("contains", 2, 2);
|
AddFunction<Contains>("contains", 2, 2);
|
||||||
AddFunction<EndsWith>("endsWith", 2, 2);
|
AddFunction<EndsWith>("endsWith", 2, 2);
|
||||||
AddFunction<Format>("format", 1, Byte.MaxValue);
|
AddFunction<Format>("format", 1, Byte.MaxValue);
|
||||||
|
|||||||
@@ -349,10 +349,6 @@ namespace GitHub.DistributedTask.Expressions2
|
|||||||
{
|
{
|
||||||
throw new ParseException(ParseExceptionKind.TooManyParameters, token: @operator, expression: context.Expression);
|
throw new ParseException(ParseExceptionKind.TooManyParameters, token: @operator, expression: context.Expression);
|
||||||
}
|
}
|
||||||
else if (functionInfo.Name.Equals("case", StringComparison.OrdinalIgnoreCase) && function.Parameters.Count % 2 == 0)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.EvenParameters, token: @operator, expression: context.Expression);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ namespace GitHub.DistributedTask.Expressions2
|
|||||||
case ParseExceptionKind.TooManyParameters:
|
case ParseExceptionKind.TooManyParameters:
|
||||||
description = "Too many parameters supplied";
|
description = "Too many parameters supplied";
|
||||||
break;
|
break;
|
||||||
case ParseExceptionKind.EvenParameters:
|
|
||||||
description = "Even number of parameters supplied, requires an odd number of parameters";
|
|
||||||
break;
|
|
||||||
case ParseExceptionKind.UnexpectedEndOfExpression:
|
case ParseExceptionKind.UnexpectedEndOfExpression:
|
||||||
description = "Unexpected end of expression";
|
description = "Unexpected end of expression";
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
ExceededMaxLength,
|
ExceededMaxLength,
|
||||||
TooFewParameters,
|
TooFewParameters,
|
||||||
TooManyParameters,
|
TooManyParameters,
|
||||||
EvenParameters,
|
|
||||||
UnexpectedEndOfExpression,
|
UnexpectedEndOfExpression,
|
||||||
UnexpectedSymbol,
|
UnexpectedSymbol,
|
||||||
UnrecognizedFunction,
|
UnrecognizedFunction,
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using GitHub.Actions.Expressions.Data;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
|
|
||||||
{
|
|
||||||
internal sealed class Case : Function
|
|
||||||
{
|
|
||||||
protected sealed override Object EvaluateCore(
|
|
||||||
EvaluationContext context,
|
|
||||||
out ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
// Validate argument count - must be odd (pairs of predicate-result plus default)
|
|
||||||
if (Parameters.Count % 2 == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("case requires an odd number of arguments");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate predicate-result pairs
|
|
||||||
for (var i = 0; i < Parameters.Count - 1; i += 2)
|
|
||||||
{
|
|
||||||
var predicate = Parameters[i].Evaluate(context);
|
|
||||||
|
|
||||||
// Predicate must be a boolean
|
|
||||||
if (predicate.Kind != ValueKind.Boolean)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("case predicate must evaluate to a boolean value");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If predicate is true, return the corresponding result
|
|
||||||
if ((Boolean)predicate.Value)
|
|
||||||
{
|
|
||||||
var result = Parameters[i + 1].Evaluate(context);
|
|
||||||
return result.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No predicate matched, return default (last argument)
|
|
||||||
var defaultResult = Parameters[Parameters.Count - 1].Evaluate(context);
|
|
||||||
return defaultResult.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
Inputs = actionToClone.Inputs?.Clone();
|
Inputs = actionToClone.Inputs?.Clone();
|
||||||
ContextName = actionToClone?.ContextName;
|
ContextName = actionToClone?.ContextName;
|
||||||
DisplayNameToken = actionToClone.DisplayNameToken?.Clone();
|
DisplayNameToken = actionToClone.DisplayNameToken?.Clone();
|
||||||
Background = actionToClone.Background;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override StepType Type => StepType.Action;
|
public override StepType Type => StepType.Action;
|
||||||
@@ -50,9 +49,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public TemplateToken Inputs { get; set; }
|
public TemplateToken Inputs { get; set; }
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public bool Background { get; set; }
|
|
||||||
|
|
||||||
public override Step Clone()
|
public override Step Clone()
|
||||||
{
|
{
|
||||||
return new ActionStep(this);
|
return new ActionStep(this);
|
||||||
|
|||||||
@@ -253,50 +253,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public bool EnableDebugger
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public DebuggerTunnelInfo DebuggerTunnel
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional welcome message shown in the debugger console when a client connects.
|
|
||||||
/// Only used when the <c>actions_runner_override_debugger_welcome_message</c>
|
|
||||||
/// feature flag is set to <c>true</c> in the job variables. With the flag set,
|
|
||||||
/// a non-empty value is shown as-is and a null or empty value suppresses the
|
|
||||||
/// default welcome message. When the flag is not set, the runner shows its
|
|
||||||
/// built-in help text and this field is ignored.
|
|
||||||
/// </summary>
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string DebuggerWelcomeMessage
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the workflow-level action dependencies (lockfile entries)
|
|
||||||
/// </summary>
|
|
||||||
public IList<String> ActionsDependencies
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_actionsDependencies == null)
|
|
||||||
{
|
|
||||||
m_actionsDependencies = new List<String>();
|
|
||||||
}
|
|
||||||
return m_actionsDependencies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection of variables associated with the current context.
|
/// Gets the collection of variables associated with the current context.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -471,11 +427,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
m_variables = null;
|
m_variables = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_actionsDependencies?.Count == 0)
|
|
||||||
{
|
|
||||||
m_actionsDependencies = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||||
if (!string.IsNullOrEmpty(m_jobContainerResourceAlias))
|
if (!string.IsNullOrEmpty(m_jobContainerResourceAlias))
|
||||||
{
|
{
|
||||||
@@ -501,9 +452,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
[DataMember(Name = "Variables", EmitDefaultValue = false)]
|
[DataMember(Name = "Variables", EmitDefaultValue = false)]
|
||||||
private IDictionary<String, VariableValue> m_variables;
|
private IDictionary<String, VariableValue> m_variables;
|
||||||
|
|
||||||
[DataMember(Name = "dependencies", EmitDefaultValue = false)]
|
|
||||||
private List<String> m_actionsDependencies;
|
|
||||||
|
|
||||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||||
[DataMember(Name = "JobSidecarContainers", EmitDefaultValue = false)]
|
[DataMember(Name = "JobSidecarContainers", EmitDefaultValue = false)]
|
||||||
private IDictionary<String, String> m_jobSidecarContainers;
|
private IDictionary<String, String> m_jobSidecarContainers;
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Pipelines
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Known control-flow types for background step control steps.
|
|
||||||
/// Wire values must match run-service constants (wait, wait-all, cancel).
|
|
||||||
/// </summary>
|
|
||||||
public static class BackgroundControlTypes
|
|
||||||
{
|
|
||||||
public const string Wait = "wait";
|
|
||||||
public const string WaitAll = "wait-all";
|
|
||||||
public const string Cancel = "cancel";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a unified background step control-flow step (wait, wait-all, cancel).
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
||||||
public class BackgroundStepControl : JobStep
|
|
||||||
{
|
|
||||||
[JsonConstructor]
|
|
||||||
public BackgroundStepControl()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private BackgroundStepControl(BackgroundStepControl stepToClone)
|
|
||||||
: base(stepToClone)
|
|
||||||
{
|
|
||||||
this.ControlType = stepToClone.ControlType;
|
|
||||||
this.StepIds = stepToClone.StepIds != null
|
|
||||||
? (string[])stepToClone.StepIds.Clone()
|
|
||||||
: null;
|
|
||||||
this.DisplayNameToken = stepToClone.DisplayNameToken?.Clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override StepType Type => StepType.BackgroundStepControl;
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string ControlType { get; set; }
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string[] StepIds { get; set; }
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public TemplateToken DisplayNameToken { get; set; }
|
|
||||||
|
|
||||||
public override Step Clone()
|
|
||||||
{
|
|
||||||
return new BackgroundStepControl(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using System.Runtime.Serialization;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Pipelines
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Dev Tunnel information the runner needs to host the debugger tunnel.
|
|
||||||
/// Matches the run-service <c>DebuggerTunnel</c> contract.
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
|
||||||
public sealed class DebuggerTunnelInfo
|
|
||||||
{
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string TunnelId { get; set; }
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string ClusterId { get; set; }
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string HostToken { get; set; }
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public ushort Port { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ namespace GitHub.DistributedTask.Pipelines.Expressions
|
|||||||
public const String Email = nameof(Email);
|
public const String Email = nameof(Email);
|
||||||
public const String IPv4Address = nameof(IPv4Address);
|
public const String IPv4Address = nameof(IPv4Address);
|
||||||
public const String SHA1 = nameof(SHA1);
|
public const String SHA1 = nameof(SHA1);
|
||||||
public const String CommitHash = nameof(CommitHash);
|
|
||||||
public const String Url = nameof(Url);
|
public const String Url = nameof(Url);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -25,8 +24,7 @@ namespace GitHub.DistributedTask.Pipelines.Expressions
|
|||||||
case IPv4Address:
|
case IPv4Address:
|
||||||
return s_validIPv4Address;
|
return s_validIPv4Address;
|
||||||
case SHA1:
|
case SHA1:
|
||||||
case CommitHash:
|
return s_validSha1;
|
||||||
return s_validCommitHash;
|
|
||||||
case Url:
|
case Url:
|
||||||
return s_validUrl;
|
return s_validUrl;
|
||||||
default:
|
default:
|
||||||
@@ -48,9 +46,9 @@ namespace GitHub.DistributedTask.Pipelines.Expressions
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 40 or 64 hex characters (SHA-1 or SHA-256 commit hash)
|
// 40 hex characters
|
||||||
private static readonly Lazy<Regex> s_validCommitHash = new Lazy<Regex>(() => new Regex(
|
private static readonly Lazy<Regex> s_validSha1 = new Lazy<Regex>(() => new Regex(
|
||||||
@"\b(?:[0-9a-f]{40}|[0-9a-f]{64})\b",
|
@"\b[0-9a-f]{40}\b",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexUtility.GetRegexTimeOut()
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexUtility.GetRegexTimeOut()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,24 +39,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the container entrypoint override.
|
|
||||||
/// </summary>
|
|
||||||
public String Entrypoint
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the container command and args (after the image name).
|
|
||||||
/// </summary>
|
|
||||||
public String Command
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the volumes which are mounted into the container.
|
/// Gets or sets the volumes which are mounted into the container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
this.Condition = stepToClone.Condition;
|
this.Condition = stepToClone.Condition;
|
||||||
this.ContinueOnError = stepToClone.ContinueOnError?.Clone();
|
this.ContinueOnError = stepToClone.ContinueOnError?.Clone();
|
||||||
this.TimeoutInMinutes = stepToClone.TimeoutInMinutes?.Clone();
|
this.TimeoutInMinutes = stepToClone.TimeoutInMinutes?.Clone();
|
||||||
this.ParallelGroupId = stepToClone.ParallelGroupId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
@@ -45,8 +44,5 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public string ParallelGroupId { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
public const String NumberStrategyContext = "number-strategy-context";
|
public const String NumberStrategyContext = "number-strategy-context";
|
||||||
public const String On = "on";
|
public const String On = "on";
|
||||||
public const String Options = "options";
|
public const String Options = "options";
|
||||||
public const String Entrypoint = "entrypoint";
|
|
||||||
public const String Command = "command";
|
|
||||||
public const String Outputs = "outputs";
|
public const String Outputs = "outputs";
|
||||||
public const String OutputsPattern = "needs.*.outputs";
|
public const String OutputsPattern = "needs.*.outputs";
|
||||||
public const String Password = "password";
|
public const String Password = "password";
|
||||||
|
|||||||
@@ -237,8 +237,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
internal static JobContainer ConvertToJobContainer(
|
internal static JobContainer ConvertToJobContainer(
|
||||||
TemplateContext context,
|
TemplateContext context,
|
||||||
TemplateToken value,
|
TemplateToken value,
|
||||||
bool allowExpressions = false,
|
bool allowExpressions = false)
|
||||||
bool allowServiceContainerCommand = false)
|
|
||||||
{
|
{
|
||||||
var result = new JobContainer();
|
var result = new JobContainer();
|
||||||
if (allowExpressions && value.Traverse().Any(x => x is ExpressionToken))
|
if (allowExpressions && value.Traverse().Any(x => x is ExpressionToken))
|
||||||
@@ -281,22 +280,6 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
case PipelineTemplateConstants.Options:
|
case PipelineTemplateConstants.Options:
|
||||||
result.Options = containerPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Container} {propertyName}").Value;
|
result.Options = containerPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Container} {propertyName}").Value;
|
||||||
break;
|
break;
|
||||||
case PipelineTemplateConstants.Entrypoint:
|
|
||||||
if (!allowServiceContainerCommand)
|
|
||||||
{
|
|
||||||
context.Error(containerPropertyPair.Key, $"The key '{PipelineTemplateConstants.Entrypoint}' is not allowed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
result.Entrypoint = containerPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Container} {propertyName}").Value;
|
|
||||||
break;
|
|
||||||
case PipelineTemplateConstants.Command:
|
|
||||||
if (!allowServiceContainerCommand)
|
|
||||||
{
|
|
||||||
context.Error(containerPropertyPair.Key, $"The key '{PipelineTemplateConstants.Command}' is not allowed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
result.Command = containerPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Container} {propertyName}").Value;
|
|
||||||
break;
|
|
||||||
case PipelineTemplateConstants.Ports:
|
case PipelineTemplateConstants.Ports:
|
||||||
var ports = containerPropertyPair.Value.AssertSequence($"{PipelineTemplateConstants.Container} {propertyName}");
|
var ports = containerPropertyPair.Value.AssertSequence($"{PipelineTemplateConstants.Container} {propertyName}");
|
||||||
var portList = new List<String>(ports.Count);
|
var portList = new List<String>(ports.Count);
|
||||||
@@ -343,8 +326,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
internal static List<KeyValuePair<String, JobContainer>> ConvertToJobServiceContainers(
|
internal static List<KeyValuePair<String, JobContainer>> ConvertToJobServiceContainers(
|
||||||
TemplateContext context,
|
TemplateContext context,
|
||||||
TemplateToken services,
|
TemplateToken services,
|
||||||
bool allowExpressions = false,
|
bool allowExpressions = false)
|
||||||
bool allowServiceContainerCommand = false)
|
|
||||||
{
|
{
|
||||||
var result = new List<KeyValuePair<String, JobContainer>>();
|
var result = new List<KeyValuePair<String, JobContainer>>();
|
||||||
|
|
||||||
@@ -358,7 +340,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
foreach (var servicePair in servicesMapping)
|
foreach (var servicePair in servicesMapping)
|
||||||
{
|
{
|
||||||
var networkAlias = servicePair.Key.AssertString("services key").Value;
|
var networkAlias = servicePair.Key.AssertString("services key").Value;
|
||||||
var container = ConvertToJobContainer(context, servicePair.Value, allowExpressions, allowServiceContainerCommand);
|
var container = ConvertToJobContainer(context, servicePair.Value);
|
||||||
result.Add(new KeyValuePair<String, JobContainer>(networkAlias, container));
|
result.Add(new KeyValuePair<String, JobContainer>(networkAlias, container));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
|
|
||||||
public Int32 MaxResultSize { get; set; } = 10 * 1024 * 1024; // 10 mb
|
public Int32 MaxResultSize { get; set; } = 10 * 1024 * 1024; // 10 mb
|
||||||
|
|
||||||
public bool AllowServiceContainerCommand { get; set; }
|
|
||||||
|
|
||||||
public Boolean EvaluateStepContinueOnError(
|
public Boolean EvaluateStepContinueOnError(
|
||||||
TemplateToken token,
|
TemplateToken token,
|
||||||
DictionaryContextData contextData,
|
DictionaryContextData contextData,
|
||||||
@@ -359,7 +357,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
{
|
{
|
||||||
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Services, token, 0, null, omitHeader: true);
|
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Services, token, 0, null, omitHeader: true);
|
||||||
context.Errors.Check();
|
context.Errors.Check();
|
||||||
result = PipelineTemplateConverter.ConvertToJobServiceContainers(context, token, allowServiceContainerCommand: AllowServiceContainerCommand);
|
result = PipelineTemplateConverter.ConvertToJobServiceContainers(context, token);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (!(ex is TemplateValidationException))
|
catch (Exception ex) when (!(ex is TemplateValidationException))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
{
|
{
|
||||||
[DataContract]
|
[DataContract]
|
||||||
[KnownType(typeof(ActionStep))]
|
[KnownType(typeof(ActionStep))]
|
||||||
[KnownType(typeof(BackgroundStepControl))]
|
|
||||||
[JsonConverter(typeof(StepConverter))]
|
[JsonConverter(typeof(StepConverter))]
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public abstract class Step
|
public abstract class Step
|
||||||
@@ -69,7 +68,5 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
{
|
{
|
||||||
[DataMember]
|
[DataMember]
|
||||||
Action = 4,
|
Action = 4,
|
||||||
[DataMember]
|
|
||||||
BackgroundStepControl = 5,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,6 @@ namespace GitHub.DistributedTask.Pipelines
|
|||||||
case StepType.Action:
|
case StepType.Action:
|
||||||
stepObject = new ActionStep();
|
stepObject = new ActionStep();
|
||||||
break;
|
break;
|
||||||
case StepType.BackgroundStepControl:
|
|
||||||
stepObject = new BackgroundStepControl();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var objectReader = value.CreateReader())
|
using (var objectReader = value.CreateReader())
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user