mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
3 Commits
chore/npm-
...
v2.327.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9513afd82f | ||
|
|
ff3fafcc38 | ||
|
|
f54fe81688 |
@@ -4,7 +4,7 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
|
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
|
||||||
"ghcr.io/devcontainers/features/dotnet": {
|
"ghcr.io/devcontainers/features/dotnet": {
|
||||||
"version": "8.0.415"
|
"version": "8.0.412"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"version": "20"
|
"version": "20"
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 🛑 Request a feature in the runner application
|
- name: 🛑 Request a feature in the runner application
|
||||||
url: https://github.com/orgs/community/discussions/categories/actions
|
url: https://github.com/orgs/community/discussions/categories/actions-and-packages
|
||||||
about: If you have feature requests for GitHub Actions, please use the Actions section on the Github Product Feedback page.
|
about: If you have feature requests for GitHub Actions, please use the Actions and Packages section on the Github Product Feedback page.
|
||||||
- name: ✅ Support for GitHub Actions
|
- name: ✅ Support for GitHub Actions
|
||||||
url: https://github.community/c/code-to-cloud/52
|
url: https://github.community/c/code-to-cloud/52
|
||||||
about: If you have questions about GitHub Actions or need support writing workflows, please ask in the GitHub Community Support forum.
|
about: If you have questions about GitHub Actions or need support writing workflows, please ask in the GitHub Community Support forum.
|
||||||
|
|||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
|
|||||||
2
.github/workflows/close-bugs-bot.yml
vendored
2
.github/workflows/close-bugs-bot.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
close-issue-message: "This issue does not seem to be a problem with the runner application, it concerns the GitHub actions platform more generally. Could you please post your feedback on the [GitHub Community Support Forum](https://github.com/orgs/community/discussions/categories/actions) which is actively monitored. Using the forum ensures that we route your problem to the correct team. 😃"
|
close-issue-message: "This issue does not seem to be a problem with the runner application, it concerns the GitHub actions platform more generally. Could you please post your feedback on the [GitHub Community Support Forum](https://github.com/orgs/community/discussions/categories/actions) which is actively monitored. Using the forum ensures that we route your problem to the correct team. 😃"
|
||||||
exempt-issue-labels: "keep"
|
exempt-issue-labels: "keep"
|
||||||
|
|||||||
4
.github/workflows/close-features-bot.yml
vendored
4
.github/workflows/close-features-bot.yml
vendored
@@ -7,9 +7,9 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
close-issue-message: "Thank you for your interest in the runner application and taking the time to provide your valuable feedback. We kindly ask you to redirect this feedback to the [GitHub Community Support Forum](https://github.com/orgs/community/discussions/categories/actions) which our team actively monitors and would be a better place to start a discussion for new feature requests in GitHub Actions. For more information on this policy please [read our contribution guidelines](https://github.com/actions/runner#contribute). 😃"
|
close-issue-message: "Thank you for your interest in the runner application and taking the time to provide your valuable feedback. We kindly ask you to redirect this feedback to the [GitHub Community Support Forum](https://github.com/orgs/community/discussions/categories/actions-and-packages) which our team actively monitors and would be a better place to start a discussion for new feature requests in GitHub Actions. For more information on this policy please [read our contribution guidelines](https://github.com/actions/runner#contribute). 😃"
|
||||||
exempt-issue-labels: "keep"
|
exempt-issue-labels: "keep"
|
||||||
stale-issue-label: "actions-feature"
|
stale-issue-label: "actions-feature"
|
||||||
only-labels: "actions-feature"
|
only-labels: "actions-feature"
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -23,11 +23,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@v3
|
||||||
# Override language selection by uncommenting this and choosing your languages
|
# Override language selection by uncommenting this and choosing your languages
|
||||||
# with:
|
# with:
|
||||||
# languages: go, javascript, csharp, python, cpp, java
|
# languages: go, javascript, csharp, python, cpp, java
|
||||||
@@ -38,4 +38,4 @@ jobs:
|
|||||||
working-directory: src
|
working-directory: src
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|||||||
211
.github/workflows/dependency-check.yml
vendored
211
.github/workflows/dependency-check.yml
vendored
@@ -1,211 +0,0 @@
|
|||||||
name: Dependency Status Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
check_type:
|
|
||||||
description: "Type of dependency check"
|
|
||||||
required: false
|
|
||||||
default: "all"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- all
|
|
||||||
- node
|
|
||||||
- dotnet
|
|
||||||
- docker
|
|
||||||
- npm
|
|
||||||
schedule:
|
|
||||||
- cron: "0 11 * * 1" # Weekly on Monday at 11 AM
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dependency-status:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
node20-status: ${{ steps.check-versions.outputs.node20-status }}
|
|
||||||
node24-status: ${{ steps.check-versions.outputs.node24-status }}
|
|
||||||
dotnet-status: ${{ steps.check-versions.outputs.dotnet-status }}
|
|
||||||
docker-status: ${{ steps.check-versions.outputs.docker-status }}
|
|
||||||
buildx-status: ${{ steps.check-versions.outputs.buildx-status }}
|
|
||||||
npm-vulnerabilities: ${{ steps.check-versions.outputs.npm-vulnerabilities }}
|
|
||||||
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Check dependency versions
|
|
||||||
id: check-versions
|
|
||||||
run: |
|
|
||||||
echo "## Dependency Status Report" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Generated on: $(date)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# Check Node versions
|
|
||||||
if [[ "${{ github.event.inputs.check_type }}" == "all" || "${{ github.event.inputs.check_type }}" == "node" ]]; then
|
|
||||||
echo "### Node.js Versions" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
VERSIONS_JSON=$(curl -s https://raw.githubusercontent.com/actions/node-versions/main/versions-manifest.json)
|
|
||||||
LATEST_NODE20=$(echo "$VERSIONS_JSON" | jq -r '.[] | select(.version | startswith("20.")) | .version' | head -1)
|
|
||||||
LATEST_NODE24=$(echo "$VERSIONS_JSON" | jq -r '.[] | select(.version | startswith("24.")) | .version' | head -1)
|
|
||||||
|
|
||||||
CURRENT_NODE20=$(grep "NODE20_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
|
|
||||||
CURRENT_NODE24=$(grep "NODE24_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
|
|
||||||
|
|
||||||
NODE20_STATUS="✅ up-to-date"
|
|
||||||
NODE24_STATUS="✅ up-to-date"
|
|
||||||
|
|
||||||
if [ "$CURRENT_NODE20" != "$LATEST_NODE20" ]; then
|
|
||||||
NODE20_STATUS="⚠️ outdated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$CURRENT_NODE24" != "$LATEST_NODE24" ]; then
|
|
||||||
NODE24_STATUS="⚠️ outdated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "| Version | Current | Latest | Status |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|---------|---------|--------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Node 20 | $CURRENT_NODE20 | $LATEST_NODE20 | $NODE20_STATUS |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Node 24 | $CURRENT_NODE24 | $LATEST_NODE24 | $NODE24_STATUS |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
echo "node20-status=$NODE20_STATUS" >> $GITHUB_OUTPUT
|
|
||||||
echo "node24-status=$NODE24_STATUS" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check .NET version
|
|
||||||
if [[ "${{ github.event.inputs.check_type }}" == "all" || "${{ github.event.inputs.check_type }}" == "dotnet" ]]; then
|
|
||||||
echo "### .NET SDK Version" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
current_dotnet_version=$(jq -r .sdk.version ./src/global.json)
|
|
||||||
current_major_minor=$(echo "$current_dotnet_version" | cut -d '.' -f 1,2)
|
|
||||||
latest_dotnet_version=$(curl -sb -H "Accept: application/json" "https://dotnetcli.blob.core.windows.net/dotnet/Sdk/$current_major_minor/latest.version")
|
|
||||||
|
|
||||||
DOTNET_STATUS="✅ up-to-date"
|
|
||||||
if [ "$current_dotnet_version" != "$latest_dotnet_version" ]; then
|
|
||||||
DOTNET_STATUS="⚠️ outdated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "| Component | Current | Latest | Status |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-----------|---------|--------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| .NET SDK | $current_dotnet_version | $latest_dotnet_version | $DOTNET_STATUS |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
echo "dotnet-status=$DOTNET_STATUS" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check Docker versions
|
|
||||||
if [[ "${{ github.event.inputs.check_type }}" == "all" || "${{ github.event.inputs.check_type }}" == "docker" ]]; then
|
|
||||||
echo "### Docker Versions" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
current_docker=$(grep "ARG DOCKER_VERSION=" ./images/Dockerfile | cut -d'=' -f2)
|
|
||||||
current_buildx=$(grep "ARG BUILDX_VERSION=" ./images/Dockerfile | cut -d'=' -f2)
|
|
||||||
|
|
||||||
latest_docker=$(curl -s https://download.docker.com/linux/static/stable/x86_64/ | grep -o 'docker-[0-9]*\.[0-9]*\.[0-9]*\.tgz' | sort -V | tail -n 1 | sed 's/docker-\(.*\)\.tgz/\1/')
|
|
||||||
latest_buildx=$(curl -s https://api.github.com/repos/docker/buildx/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
|
||||||
|
|
||||||
DOCKER_STATUS="✅ up-to-date"
|
|
||||||
BUILDX_STATUS="✅ up-to-date"
|
|
||||||
|
|
||||||
if [ "$current_docker" != "$latest_docker" ]; then
|
|
||||||
DOCKER_STATUS="⚠️ outdated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$current_buildx" != "$latest_buildx" ]; then
|
|
||||||
BUILDX_STATUS="⚠️ outdated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "| Component | Current | Latest | Status |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-----------|---------|--------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Docker | $current_docker | $latest_docker | $DOCKER_STATUS |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Docker Buildx | $current_buildx | $latest_buildx | $BUILDX_STATUS |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
echo "docker-status=$DOCKER_STATUS" >> $GITHUB_OUTPUT
|
|
||||||
echo "buildx-status=$BUILDX_STATUS" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check npm vulnerabilities
|
|
||||||
if [[ "${{ github.event.inputs.check_type }}" == "all" || "${{ github.event.inputs.check_type }}" == "npm" ]]; then
|
|
||||||
echo "### NPM Security Audit" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
cd src/Misc/expressionFunc/hashFiles
|
|
||||||
npm install --silent
|
|
||||||
|
|
||||||
AUDIT_OUTPUT=""
|
|
||||||
AUDIT_EXIT_CODE=0
|
|
||||||
# Run npm audit and capture output and exit code
|
|
||||||
if ! AUDIT_OUTPUT=$(npm audit --json 2>&1); then
|
|
||||||
AUDIT_EXIT_CODE=$?
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if output is valid JSON
|
|
||||||
if echo "$AUDIT_OUTPUT" | jq . >/dev/null 2>&1; then
|
|
||||||
VULN_COUNT=$(echo "$AUDIT_OUTPUT" | jq '.metadata.vulnerabilities.total // 0')
|
|
||||||
# Ensure VULN_COUNT is a number
|
|
||||||
VULN_COUNT=$(echo "$VULN_COUNT" | grep -o '[0-9]*' | head -1)
|
|
||||||
VULN_COUNT=${VULN_COUNT:-0}
|
|
||||||
|
|
||||||
NPM_STATUS="✅ no vulnerabilities"
|
|
||||||
if [ "$VULN_COUNT" -gt 0 ] 2>/dev/null; then
|
|
||||||
NPM_STATUS="⚠️ $VULN_COUNT vulnerabilities found"
|
|
||||||
|
|
||||||
# Get vulnerability details
|
|
||||||
HIGH_VULNS=$(echo "$AUDIT_OUTPUT" | jq '.metadata.vulnerabilities.high // 0')
|
|
||||||
CRITICAL_VULNS=$(echo "$AUDIT_OUTPUT" | jq '.metadata.vulnerabilities.critical // 0')
|
|
||||||
|
|
||||||
echo "| Severity | Count |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Critical | $CRITICAL_VULNS |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| High | $HIGH_VULNS |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "No npm vulnerabilities found ✅" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
NPM_STATUS="❌ npm audit failed"
|
|
||||||
echo "npm audit failed to run or returned invalid JSON ❌" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Exit code: $AUDIT_EXIT_CODE" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Output: $AUDIT_OUTPUT" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "npm-vulnerabilities=$NPM_STATUS" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check for open dependency PRs
|
|
||||||
id: check-prs
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
echo "### Open Dependency PRs" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# Get open PRs with dependency label
|
|
||||||
OPEN_PRS=$(gh pr list --label "dependencies" --state open --json number,title,url)
|
|
||||||
PR_COUNT=$(echo "$OPEN_PRS" | jq '. | length')
|
|
||||||
|
|
||||||
if [ "$PR_COUNT" -gt 0 ]; then
|
|
||||||
echo "Found $PR_COUNT open dependency PR(s):" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "$OPEN_PRS" | jq -r '.[] | "- [#\(.number)](\(.url)) \(.title)"' >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "No open dependency PRs found ✅" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "open-dependency-prs=$PR_COUNT" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
echo "### Summary" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- Check for open PRs with the \`dependency\` label before releases" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- Review and merge dependency updates regularly" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- Critical vulnerabilities should be addressed immediately" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "**Automated workflows run weekly to check for updates:**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- Node.js versions (Mondays at 6 AM)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- NPM audit fix (Mondays at 7 AM)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- .NET SDK updates (Mondays at midnight)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- Docker/Buildx updates (Mondays at midnight)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
62
.github/workflows/docker-buildx-upgrade.yml
vendored
62
.github/workflows/docker-buildx-upgrade.yml
vendored
@@ -2,8 +2,8 @@ name: "Docker/Buildx Version Upgrade"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * 1" # Run every Monday at midnight
|
- cron: '0 0 * * 1' # Run every Monday at midnight
|
||||||
workflow_dispatch: # Allow manual triggering
|
workflow_dispatch: # Allow manual triggering
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-versions:
|
check-versions:
|
||||||
@@ -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@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check Docker version
|
- name: Check Docker version
|
||||||
id: check_docker_version
|
id: check_docker_version
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
echo "Failed to retrieve a valid Docker version"
|
echo "Failed to retrieve a valid Docker version"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
should_update=0
|
should_update=0
|
||||||
[ "$current_version" != "$latest_version" ] && should_update=1
|
[ "$current_version" != "$latest_version" ] && should_update=1
|
||||||
|
|
||||||
@@ -64,17 +64,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker_should_update="${{ steps.check_docker_version.outputs.SHOULD_UPDATE }}"
|
docker_should_update="${{ steps.check_docker_version.outputs.SHOULD_UPDATE }}"
|
||||||
buildx_should_update="${{ steps.check_buildx_version.outputs.SHOULD_UPDATE }}"
|
buildx_should_update="${{ steps.check_buildx_version.outputs.SHOULD_UPDATE }}"
|
||||||
|
|
||||||
# Show annotation if only Docker needs update
|
# Show annotation if only Docker needs update
|
||||||
if [[ "$docker_should_update" == "1" && "$buildx_should_update" == "0" ]]; then
|
if [[ "$docker_should_update" == "1" && "$buildx_should_update" == "0" ]]; then
|
||||||
echo "::warning ::Docker version (${{ steps.check_docker_version.outputs.LATEST_VERSION }}) needs update but Buildx is current. Only updating when both need updates."
|
echo "::warning ::Docker version (${{ steps.check_docker_version.outputs.LATEST_VERSION }}) needs update but Buildx is current. Only updating when both need updates."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Show annotation if only Buildx needs update
|
# Show annotation if only Buildx needs update
|
||||||
if [[ "$docker_should_update" == "0" && "$buildx_should_update" == "1" ]]; then
|
if [[ "$docker_should_update" == "0" && "$buildx_should_update" == "1" ]]; then
|
||||||
echo "::warning ::Buildx version (${{ steps.check_buildx_version.outputs.LATEST_VERSION }}) needs update but Docker is current. Only updating when both need updates."
|
echo "::warning ::Buildx version (${{ steps.check_buildx_version.outputs.LATEST_VERSION }}) needs update but Docker is current. Only updating when both need updates."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Show annotation when both are current
|
# Show annotation when both are current
|
||||||
if [[ "$docker_should_update" == "0" && "$buildx_should_update" == "0" ]]; then
|
if [[ "$docker_should_update" == "0" && "$buildx_should_update" == "0" ]]; then
|
||||||
echo "::warning ::Latest Docker version is ${{ steps.check_docker_version.outputs.LATEST_VERSION }} and Buildx version is ${{ steps.check_buildx_version.outputs.LATEST_VERSION }}. No updates needed."
|
echo "::warning ::Latest Docker version is ${{ steps.check_docker_version.outputs.LATEST_VERSION }} and Buildx version is ${{ steps.check_buildx_version.outputs.LATEST_VERSION }}. No updates needed."
|
||||||
@@ -89,26 +89,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Update Docker version
|
- name: Update Docker version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
latest_version="${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }}"
|
latest_version="${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }}"
|
||||||
current_version="${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }}"
|
current_version="${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }}"
|
||||||
|
|
||||||
# Update version in Dockerfile
|
# Update version in Dockerfile
|
||||||
sed -i "s/ARG DOCKER_VERSION=$current_version/ARG DOCKER_VERSION=$latest_version/g" ./images/Dockerfile
|
sed -i "s/ARG DOCKER_VERSION=$current_version/ARG DOCKER_VERSION=$latest_version/g" ./images/Dockerfile
|
||||||
|
|
||||||
- name: Update Buildx version
|
- name: Update Buildx version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
latest_version="${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
|
latest_version="${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
|
||||||
current_version="${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }}"
|
current_version="${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }}"
|
||||||
|
|
||||||
# Update version in Dockerfile
|
# Update version in Dockerfile
|
||||||
sed -i "s/ARG BUILDX_VERSION=$current_version/ARG BUILDX_VERSION=$latest_version/g" ./images/Dockerfile
|
sed -i "s/ARG BUILDX_VERSION=$current_version/ARG BUILDX_VERSION=$latest_version/g" ./images/Dockerfile
|
||||||
|
|
||||||
- name: Commit changes and create Pull Request
|
- name: Commit changes and create Pull Request
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
branch_name="feature/docker-buildx-upgrade"
|
branch_name="feature/docker-buildx-upgrade"
|
||||||
commit_message="Upgrade Docker to v${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Buildx to v${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
|
commit_message="Upgrade Docker to v${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Buildx to v${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
|
||||||
pr_title="Update Docker to v${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Buildx to v${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
|
pr_title="Update Docker to v${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Buildx to v${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}"
|
||||||
|
|
||||||
# Configure git
|
# Configure git
|
||||||
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>"
|
||||||
@@ -129,38 +129,16 @@ jobs:
|
|||||||
else
|
else
|
||||||
git checkout -b "$branch_name"
|
git checkout -b "$branch_name"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Commit and push changes
|
# Commit and push changes
|
||||||
git commit -a -m "$commit_message"
|
git commit -a -m "$commit_message"
|
||||||
git push --force origin "$branch_name"
|
git push --force origin "$branch_name"
|
||||||
|
|
||||||
# Create PR body using here-doc for proper formatting
|
|
||||||
cat > pr_body.txt << 'EOF'
|
|
||||||
Automated Docker and Buildx version update:
|
|
||||||
|
|
||||||
- Docker: ${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }} → ${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }}
|
|
||||||
- Buildx: ${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }} → ${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}
|
|
||||||
|
|
||||||
This update ensures we're using the latest stable Docker and Buildx versions for security and performance improvements.
|
|
||||||
|
|
||||||
**Release notes:** https://docs.docker.com/engine/release-notes/
|
|
||||||
|
|
||||||
**Next steps:**
|
|
||||||
- Review the version changes
|
|
||||||
- Verify container builds work as expected
|
|
||||||
- Test multi-platform builds if applicable
|
|
||||||
- Merge when ready
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Autogenerated by [Docker/Buildx Version Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/docker-buildx-upgrade.yml)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Create PR
|
# Create PR
|
||||||
|
pr_body="Upgrades Docker version from ${{ needs.check-versions.outputs.DOCKER_CURRENT_VERSION }} to ${{ needs.check-versions.outputs.DOCKER_LATEST_VERSION }} and Docker Buildx version from ${{ needs.check-versions.outputs.BUILDX_CURRENT_VERSION }} to ${{ needs.check-versions.outputs.BUILDX_LATEST_VERSION }}.\n\n"
|
||||||
|
pr_body+="Release notes: https://docs.docker.com/engine/release-notes/\n\n"
|
||||||
|
pr_body+="---\n\nAutogenerated by [Docker/Buildx Version Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/docker-buildx-upgrade.yml)"
|
||||||
|
|
||||||
gh pr create -B main -H "$branch_name" \
|
gh pr create -B main -H "$branch_name" \
|
||||||
--title "$pr_title" \
|
--title "$pr_title" \
|
||||||
--label "dependencies" \
|
--body "$pr_body"
|
||||||
--label "dependencies-weekly-check" \
|
|
||||||
--label "dependencies-not-dependabot" \
|
|
||||||
--label "docker" \
|
|
||||||
--body-file pr_body.txt
|
|
||||||
|
|||||||
34
.github/workflows/dotnet-upgrade.yml
vendored
34
.github/workflows/dotnet-upgrade.yml
vendored
@@ -2,20 +2,20 @@ name: "DotNet SDK Upgrade"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 8 * * 1" # Weekly on Monday at 8 AM UTC (independent of Node.js/NPM)
|
- cron: '0 0 * * 1'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dotnet-update:
|
dotnet-update:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
SHOULD_UPDATE: ${{ steps.fetch_latest_version.outputs.SHOULD_UPDATE }}
|
SHOULD_UPDATE: ${{ steps.fetch_latest_version.outputs.SHOULD_UPDATE }}
|
||||||
BRANCH_EXISTS: ${{ steps.fetch_latest_version.outputs.BRANCH_EXISTS }}
|
BRANCH_EXISTS: ${{ steps.fetch_latest_version.outputs.BRANCH_EXISTS }}
|
||||||
DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION: ${{ steps.fetch_latest_version.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION: ${{ steps.fetch_latest_version.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
|
||||||
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@v5
|
uses: actions/checkout@v4
|
||||||
- name: Get current major minor version
|
- name: Get current major minor version
|
||||||
id: fetch_current_version
|
id: fetch_current_version
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
# check if git branch already exists for the upgrade
|
# check if git branch already exists for the upgrade
|
||||||
branch_already_exists=0
|
branch_already_exists=0
|
||||||
|
|
||||||
if git ls-remote --heads --exit-code origin refs/heads/feature/dotnetsdk-upgrade/${latest_patch_version};
|
if git ls-remote --heads --exit-code origin refs/heads/feature/dotnetsdk-upgrade/${latest_patch_version};
|
||||||
then
|
then
|
||||||
branch_already_exists=1
|
branch_already_exists=1
|
||||||
@@ -89,17 +89,17 @@ 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@v5
|
- uses: actions/checkout@v4
|
||||||
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
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
gh pr create -B main -H feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }} --title "Update dotnet sdk to latest version @${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}" --label "dependencies" --label "dependencies-weekly-check" --label "dependencies-not-dependabot" --label "dotnet" --body "
|
gh pr create -B main -H feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }} --title "Update dotnet sdk to latest version @${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}" --body "
|
||||||
https://dotnetcli.blob.core.windows.net/dotnet/Sdk/${{ needs.dotnet-update.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}/latest.version
|
https://dotnetcli.blob.core.windows.net/dotnet/Sdk/${{ needs.dotnet-update.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}/latest.version
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
---
|
|
||||||
|
Autogenerated by [DotNet SDK Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/dotnet-upgrade.yml)"
|
||||||
Autogenerated by [DotNet SDK Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/dotnet-upgrade.yml)"
|
|
||||||
|
|||||||
194
.github/workflows/node-upgrade.yml
vendored
194
.github/workflows/node-upgrade.yml
vendored
@@ -1,194 +0,0 @@
|
|||||||
name: Auto Update Node Version
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 6 * * 1" # Weekly, every Monday
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-node:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Get latest Node versions
|
|
||||||
id: node-versions
|
|
||||||
run: |
|
|
||||||
# Get latest Node.js releases from official GitHub releases
|
|
||||||
echo "Fetching latest Node.js releases..."
|
|
||||||
|
|
||||||
# Get latest v20.x release
|
|
||||||
LATEST_NODE20=$(curl -s https://api.github.com/repos/nodejs/node/releases | \
|
|
||||||
jq -r '.[] | select(.tag_name | startswith("v20.")) | .tag_name' | \
|
|
||||||
head -1 | sed 's/^v//')
|
|
||||||
|
|
||||||
# Get latest v24.x release
|
|
||||||
LATEST_NODE24=$(curl -s https://api.github.com/repos/nodejs/node/releases | \
|
|
||||||
jq -r '.[] | select(.tag_name | startswith("v24.")) | .tag_name' | \
|
|
||||||
head -1 | sed 's/^v//')
|
|
||||||
|
|
||||||
echo "Found Node.js releases: 20=$LATEST_NODE20, 24=$LATEST_NODE24"
|
|
||||||
|
|
||||||
# Verify these versions are available in alpine_nodejs releases
|
|
||||||
echo "Verifying availability in alpine_nodejs..."
|
|
||||||
ALPINE_RELEASES=$(curl -s https://api.github.com/repos/actions/alpine_nodejs/releases | jq -r '.[].tag_name')
|
|
||||||
|
|
||||||
if ! echo "$ALPINE_RELEASES" | grep -q "^v$LATEST_NODE20$"; then
|
|
||||||
echo "::warning title=Node 20 Fallback::Node 20 version $LATEST_NODE20 not found in alpine_nodejs releases, using fallback"
|
|
||||||
# Fall back to latest available alpine_nodejs v20 release
|
|
||||||
LATEST_NODE20=$(echo "$ALPINE_RELEASES" | grep "^v20\." | head -1 | sed 's/^v//')
|
|
||||||
echo "Using latest available alpine_nodejs Node 20: $LATEST_NODE20"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! echo "$ALPINE_RELEASES" | grep -q "^v$LATEST_NODE24$"; then
|
|
||||||
echo "::warning title=Node 24 Fallback::Node 24 version $LATEST_NODE24 not found in alpine_nodejs releases, using fallback"
|
|
||||||
# Fall back to latest available alpine_nodejs v24 release
|
|
||||||
LATEST_NODE24=$(echo "$ALPINE_RELEASES" | grep "^v24\." | head -1 | sed 's/^v//')
|
|
||||||
echo "Using latest available alpine_nodejs Node 24: $LATEST_NODE24"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate that we have non-empty version numbers
|
|
||||||
if [ -z "$LATEST_NODE20" ] || [ "$LATEST_NODE20" = "" ]; then
|
|
||||||
echo "::error title=Invalid Node 20 Version::Failed to determine valid Node 20 version. Got: '$LATEST_NODE20'"
|
|
||||||
echo "Available alpine_nodejs releases:"
|
|
||||||
echo "$ALPINE_RELEASES" | head -10
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$LATEST_NODE24" ] || [ "$LATEST_NODE24" = "" ]; then
|
|
||||||
echo "::error title=Invalid Node 24 Version::Failed to determine valid Node 24 version. Got: '$LATEST_NODE24'"
|
|
||||||
echo "Available alpine_nodejs releases:"
|
|
||||||
echo "$ALPINE_RELEASES" | head -10
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Additional validation: ensure versions match expected format (x.y.z)
|
|
||||||
if ! echo "$LATEST_NODE20" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
echo "::error title=Invalid Node 20 Format::Node 20 version '$LATEST_NODE20' does not match expected format (x.y.z)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! echo "$LATEST_NODE24" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
echo "::error title=Invalid Node 24 Format::Node 24 version '$LATEST_NODE24' does not match expected format (x.y.z)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Validated Node versions: 20=$LATEST_NODE20, 24=$LATEST_NODE24"
|
|
||||||
echo "latest_node20=$LATEST_NODE20" >> $GITHUB_OUTPUT
|
|
||||||
echo "latest_node24=$LATEST_NODE24" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Check current versions in externals.sh
|
|
||||||
CURRENT_NODE20=$(grep "NODE20_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
|
|
||||||
CURRENT_NODE24=$(grep "NODE24_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
|
|
||||||
|
|
||||||
echo "current_node20=$CURRENT_NODE20" >> $GITHUB_OUTPUT
|
|
||||||
echo "current_node24=$CURRENT_NODE24" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Determine if updates are needed
|
|
||||||
NEEDS_UPDATE20="false"
|
|
||||||
NEEDS_UPDATE24="false"
|
|
||||||
|
|
||||||
if [ "$CURRENT_NODE20" != "$LATEST_NODE20" ]; then
|
|
||||||
NEEDS_UPDATE20="true"
|
|
||||||
echo "::notice title=Node 20 Update Available::Current: $CURRENT_NODE20 → Latest: $LATEST_NODE20"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$CURRENT_NODE24" != "$LATEST_NODE24" ]; then
|
|
||||||
NEEDS_UPDATE24="true"
|
|
||||||
echo "::notice title=Node 24 Update Available::Current: $CURRENT_NODE24 → Latest: $LATEST_NODE24"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$NEEDS_UPDATE20" == "false" ] && [ "$NEEDS_UPDATE24" == "false" ]; then
|
|
||||||
echo "::notice title=No Updates Needed::All Node.js versions are up to date"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "needs_update20=$NEEDS_UPDATE20" >> $GITHUB_OUTPUT
|
|
||||||
echo "needs_update24=$NEEDS_UPDATE24" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Update externals.sh and create PR
|
|
||||||
if: steps.node-versions.outputs.needs_update20 == 'true' || steps.node-versions.outputs.needs_update24 == 'true'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# Final validation before making changes
|
|
||||||
NODE20_VERSION="${{ steps.node-versions.outputs.latest_node20 }}"
|
|
||||||
NODE24_VERSION="${{ steps.node-versions.outputs.latest_node24 }}"
|
|
||||||
|
|
||||||
echo "Final validation of versions before PR creation:"
|
|
||||||
echo "Node 20: '$NODE20_VERSION'"
|
|
||||||
echo "Node 24: '$NODE24_VERSION'"
|
|
||||||
|
|
||||||
# Validate versions are not empty and match expected format
|
|
||||||
if [ -z "$NODE20_VERSION" ] || ! echo "$NODE20_VERSION" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
echo "::error title=Invalid Node 20 Version::Refusing to create PR with invalid Node 20 version: '$NODE20_VERSION'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$NODE24_VERSION" ] || ! echo "$NODE24_VERSION" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
||||||
echo "::error title=Invalid Node 24 Version::Refusing to create PR with invalid Node 24 version: '$NODE24_VERSION'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ All versions validated successfully"
|
|
||||||
|
|
||||||
# Update the files
|
|
||||||
if [ "${{ steps.node-versions.outputs.needs_update20 }}" == "true" ]; then
|
|
||||||
sed -i 's/NODE20_VERSION="[^"]*"/NODE20_VERSION="'"$NODE20_VERSION"'"/' src/Misc/externals.sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${{ steps.node-versions.outputs.needs_update24 }}" == "true" ]; then
|
|
||||||
sed -i 's/NODE24_VERSION="[^"]*"/NODE24_VERSION="'"$NODE24_VERSION"'"/' src/Misc/externals.sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify the changes were applied correctly
|
|
||||||
echo "Verifying changes in externals.sh:"
|
|
||||||
grep "NODE20_VERSION=" src/Misc/externals.sh
|
|
||||||
grep "NODE24_VERSION=" src/Misc/externals.sh
|
|
||||||
|
|
||||||
# Ensure we actually have valid versions in the file
|
|
||||||
UPDATED_NODE20=$(grep "NODE20_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
|
|
||||||
UPDATED_NODE24=$(grep "NODE24_VERSION=" src/Misc/externals.sh | cut -d'"' -f2)
|
|
||||||
|
|
||||||
if [ -z "$UPDATED_NODE20" ] || [ -z "$UPDATED_NODE24" ]; then
|
|
||||||
echo "::error title=Update Failed::Failed to properly update externals.sh"
|
|
||||||
echo "Updated Node 20: '$UPDATED_NODE20'"
|
|
||||||
echo "Updated Node 24: '$UPDATED_NODE24'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Configure git
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>"
|
|
||||||
|
|
||||||
# Create branch and commit changes
|
|
||||||
branch_name="chore/update-node"
|
|
||||||
git checkout -b "$branch_name"
|
|
||||||
git commit -a -m "chore: update Node versions (20: $NODE20_VERSION, 24: $NODE24_VERSION)"
|
|
||||||
git push --force origin "$branch_name"
|
|
||||||
|
|
||||||
# Create PR body using here-doc for proper formatting
|
|
||||||
cat > pr_body.txt << EOF
|
|
||||||
Automated Node.js version update:
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
**Note**: When updating Node versions, remember to also create a new release of alpine_nodejs at the updated version following the instructions at: https://github.com/actions/alpine_nodejs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Autogenerated by [Node Version Upgrade Workflow](https://github.com/actions/runner/blob/main/.github/workflows/node-upgrade.yml)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Create PR
|
|
||||||
gh pr create -B main -H "$branch_name" \
|
|
||||||
--title "chore: update Node versions" \
|
|
||||||
--label "dependencies" \
|
|
||||||
--label "dependencies-weekly-check" \
|
|
||||||
--label "dependencies-not-dependabot" \
|
|
||||||
--label "node" \
|
|
||||||
--label "javascript" \
|
|
||||||
--body-file pr_body.txt
|
|
||||||
|
|
||||||
echo "::notice title=PR Created::Successfully created Node.js version update PR on branch $branch_name"
|
|
||||||
235
.github/workflows/npm-audit-typescript.yml
vendored
235
.github/workflows/npm-audit-typescript.yml
vendored
@@ -1,235 +0,0 @@
|
|||||||
name: NPM Audit Fix with TypeScript Auto-Fix
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
npm-audit-with-ts-fix:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
- name: NPM install and audit fix with TypeScript auto-repair
|
|
||||||
working-directory: src/Misc/expressionFunc/hashFiles
|
|
||||||
run: |
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Check for vulnerabilities first
|
|
||||||
echo "Checking for npm vulnerabilities..."
|
|
||||||
if npm audit --audit-level=moderate; then
|
|
||||||
echo "✅ No moderate or higher vulnerabilities found"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "⚠️ Vulnerabilities found, attempting npm audit fix..."
|
|
||||||
|
|
||||||
# Attempt audit fix and capture the result
|
|
||||||
if npm audit fix; then
|
|
||||||
echo "✅ npm audit fix completed successfully"
|
|
||||||
AUDIT_FIX_STATUS="success"
|
|
||||||
else
|
|
||||||
echo "⚠️ npm audit fix failed or had issues"
|
|
||||||
AUDIT_FIX_STATUS="failed"
|
|
||||||
|
|
||||||
# Try audit fix with --force as a last resort for critical/high vulns only
|
|
||||||
echo "Checking if critical/high vulnerabilities remain..."
|
|
||||||
if ! npm audit --audit-level=high; then
|
|
||||||
echo "🚨 Critical/high vulnerabilities remain, attempting --force fix..."
|
|
||||||
if npm audit fix --force; then
|
|
||||||
echo "⚠️ npm audit fix --force completed (may have breaking changes)"
|
|
||||||
AUDIT_FIX_STATUS="force-fixed"
|
|
||||||
else
|
|
||||||
echo "❌ npm audit fix --force also failed"
|
|
||||||
AUDIT_FIX_STATUS="force-failed"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "✅ Only moderate/low vulnerabilities remain after failed fix"
|
|
||||||
AUDIT_FIX_STATUS="partial-success"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "AUDIT_FIX_STATUS=$AUDIT_FIX_STATUS" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Try to fix TypeScript issues automatically
|
|
||||||
echo "Attempting to fix TypeScript compatibility issues..."
|
|
||||||
|
|
||||||
# Check if build fails
|
|
||||||
if ! npm run build 2>/dev/null; then
|
|
||||||
echo "Build failed, attempting automated fixes..."
|
|
||||||
|
|
||||||
# Common fix 1: Update @types/node to latest compatible version
|
|
||||||
echo "Trying to update @types/node to latest version..."
|
|
||||||
npm update @types/node
|
|
||||||
|
|
||||||
# Common fix 2: If that doesn't work, try installing a specific known-good version
|
|
||||||
if ! npm run build 2>/dev/null; then
|
|
||||||
echo "Trying specific @types/node version..."
|
|
||||||
# Try Node 20 compatible version
|
|
||||||
npm install --save-dev @types/node@^20.0.0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Common fix 3: Clear node_modules and reinstall if still failing
|
|
||||||
if ! npm run build 2>/dev/null; then
|
|
||||||
echo "Clearing node_modules and reinstalling..."
|
|
||||||
rm -rf node_modules package-lock.json
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Re-run audit fix after clean install if it was successful before
|
|
||||||
if [[ "$AUDIT_FIX_STATUS" == "success" || "$AUDIT_FIX_STATUS" == "force-fixed" ]]; then
|
|
||||||
echo "Re-running npm audit fix after clean install..."
|
|
||||||
npm audit fix || echo "Audit fix failed on second attempt"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Common fix 4: Try updating TypeScript itself
|
|
||||||
if ! npm run build 2>/dev/null; then
|
|
||||||
echo "Trying to update TypeScript..."
|
|
||||||
npm update typescript
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Final check
|
|
||||||
if npm run build 2>/dev/null; then
|
|
||||||
echo "✅ Successfully fixed TypeScript issues automatically"
|
|
||||||
else
|
|
||||||
echo "⚠️ Could not automatically fix TypeScript issues"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "✅ Build passes after audit fix"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create PR if changes exist
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
HUSKY: 0 # Disable husky hooks for automated commits
|
|
||||||
run: |
|
|
||||||
# Check if there are any changes
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
# Configure git
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>"
|
|
||||||
|
|
||||||
# Create branch and commit changes
|
|
||||||
branch_name="chore/npm-audit-fix-with-ts-repair"
|
|
||||||
git checkout -b "$branch_name"
|
|
||||||
|
|
||||||
# Commit with --no-verify to skip husky hooks
|
|
||||||
git commit -a -m "chore: npm audit fix with automated TypeScript compatibility fixes" --no-verify
|
|
||||||
git push --force origin "$branch_name"
|
|
||||||
|
|
||||||
# Check final build status and gather info about what was changed
|
|
||||||
build_status="✅ Build passes"
|
|
||||||
fixes_applied=""
|
|
||||||
cd src/Misc/expressionFunc/hashFiles
|
|
||||||
|
|
||||||
# Check what packages were updated
|
|
||||||
if git diff HEAD~1 package.json | grep -q "@types/node"; then
|
|
||||||
fixes_applied+="\n- Updated @types/node version for TypeScript compatibility"
|
|
||||||
fi
|
|
||||||
if git diff HEAD~1 package.json | grep -q "typescript"; then
|
|
||||||
fixes_applied+="\n- Updated TypeScript version"
|
|
||||||
fi
|
|
||||||
if git diff HEAD~1 package-lock.json | grep -q "resolved"; then
|
|
||||||
fixes_applied+="\n- Updated package dependencies via npm audit fix"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! npm run build 2>/dev/null; then
|
|
||||||
build_status="⚠️ Build fails - manual review required"
|
|
||||||
fi
|
|
||||||
cd - > /dev/null
|
|
||||||
|
|
||||||
# Create enhanced PR body using here-doc for proper formatting
|
|
||||||
audit_status_msg=""
|
|
||||||
case "$AUDIT_FIX_STATUS" in
|
|
||||||
"success")
|
|
||||||
audit_status_msg="✅ **Audit Fix**: Completed successfully"
|
|
||||||
;;
|
|
||||||
"partial-success")
|
|
||||||
audit_status_msg="⚠️ **Audit Fix**: Partial success (only moderate/low vulnerabilities remain)"
|
|
||||||
;;
|
|
||||||
"force-fixed")
|
|
||||||
audit_status_msg="⚠️ **Audit Fix**: Completed with --force (may have breaking changes)"
|
|
||||||
;;
|
|
||||||
"failed"|"force-failed")
|
|
||||||
audit_status_msg="❌ **Audit Fix**: Failed to resolve vulnerabilities"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
audit_status_msg="❓ **Audit Fix**: Status unknown"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ "$build_status" == *"fails"* ]]; then
|
|
||||||
cat > pr_body.txt << EOF
|
|
||||||
Automated npm audit fix with TypeScript auto-repair for hashFiles dependencies.
|
|
||||||
|
|
||||||
**Build Status**: ⚠️ Build fails - manual review required
|
|
||||||
$audit_status_msg
|
|
||||||
|
|
||||||
This workflow attempts to automatically fix TypeScript compatibility issues that may arise from npm audit fixes.
|
|
||||||
|
|
||||||
⚠️ **Manual Review Required**: The build is currently failing after automated fixes were attempted.
|
|
||||||
|
|
||||||
Common issues and solutions:
|
|
||||||
- Check for TypeScript version compatibility with Node.js types
|
|
||||||
- Review breaking changes in updated dependencies
|
|
||||||
- Consider pinning problematic dependency versions temporarily
|
|
||||||
- Review tsconfig.json for compatibility settings
|
|
||||||
|
|
||||||
**Automated Fix Strategy**:
|
|
||||||
1. Run npm audit fix with proper error handling
|
|
||||||
2. Update @types/node to latest compatible version
|
|
||||||
3. Try Node 20 specific @types/node version if needed
|
|
||||||
4. Clean reinstall dependencies if conflicts persist
|
|
||||||
5. Update TypeScript compiler if necessary
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Autogenerated by [NPM Audit Fix with TypeScript Auto-Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit-ts-fix.yml)
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
cat > pr_body.txt << EOF
|
|
||||||
Automated npm audit fix with TypeScript auto-repair for hashFiles dependencies.
|
|
||||||
|
|
||||||
**Build Status**: ✅ Build passes
|
|
||||||
$audit_status_msg
|
|
||||||
|
|
||||||
This workflow attempts to automatically fix TypeScript compatibility issues that may arise from npm audit fixes.
|
|
||||||
|
|
||||||
✅ **Ready to Merge**: All automated fixes were successful and the build passes.
|
|
||||||
|
|
||||||
**Automated Fix Strategy**:
|
|
||||||
1. Run npm audit fix with proper error handling
|
|
||||||
2. Update @types/node to latest compatible version
|
|
||||||
3. Try Node 20 specific @types/node version if needed
|
|
||||||
4. Clean reinstall dependencies if conflicts persist
|
|
||||||
5. Update TypeScript compiler if necessary
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Autogenerated by [NPM Audit Fix with TypeScript Auto-Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit-ts-fix.yml)
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$fixes_applied" ]; then
|
|
||||||
# Add the fixes applied section to the file
|
|
||||||
sed -i "/This workflow attempts/a\\
|
|
||||||
\\
|
|
||||||
**Automated Fixes Applied**:$fixes_applied" pr_body.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create PR with appropriate labels
|
|
||||||
labels="dependencies,dependencies-not-dependabot,typescript,npm,security"
|
|
||||||
if [[ "$build_status" == *"fails"* ]]; then
|
|
||||||
labels="dependencies,dependencies-not-dependabot,typescript,npm,security,needs-manual-review"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create PR
|
|
||||||
gh pr create -B main -H "$branch_name" \
|
|
||||||
--title "chore: npm audit fix with TypeScript auto-repair" \
|
|
||||||
--label "$labels" \
|
|
||||||
--body-file pr_body.txt
|
|
||||||
else
|
|
||||||
echo "No changes to commit"
|
|
||||||
fi
|
|
||||||
137
.github/workflows/npm-audit.yml
vendored
137
.github/workflows/npm-audit.yml
vendored
@@ -1,137 +0,0 @@
|
|||||||
name: NPM Audit Fix
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 7 * * 1" # Weekly on Monday at 7 AM UTC
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
npm-audit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: NPM install and audit fix
|
|
||||||
working-directory: src/Misc/expressionFunc/hashFiles
|
|
||||||
run: |
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Check what vulnerabilities exist
|
|
||||||
echo "=== Checking current vulnerabilities ==="
|
|
||||||
npm audit || true
|
|
||||||
|
|
||||||
# Apply audit fix --force to get security updates
|
|
||||||
echo "=== Applying npm audit fix --force ==="
|
|
||||||
npm audit fix --force
|
|
||||||
|
|
||||||
# Test if build still works and set status
|
|
||||||
echo "=== Testing build compatibility ==="
|
|
||||||
if npm run all; then
|
|
||||||
echo "✅ Build successful after audit fix"
|
|
||||||
echo "AUDIT_FIX_STATUS=success" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "❌ Build failed after audit fix - will create PR with fix instructions"
|
|
||||||
echo "AUDIT_FIX_STATUS=build_failed" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create PR if changes exist
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
# Check if there are any changes
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
# Configure git
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
# Create branch and commit changes
|
|
||||||
branch_name="chore/npm-audit-fix-$(date +%Y%m%d)"
|
|
||||||
git checkout -b "$branch_name"
|
|
||||||
git add .
|
|
||||||
git commit -m "chore: npm audit fix for hashFiles dependencies" --no-verify
|
|
||||||
git push origin "$branch_name"
|
|
||||||
|
|
||||||
# Create PR body based on what actually happened
|
|
||||||
if [ "$AUDIT_FIX_STATUS" = "success" ]; then
|
|
||||||
cat > pr_body.txt << 'EOF'
|
|
||||||
Automated npm audit fix for security vulnerabilities in hashFiles dependencies.
|
|
||||||
|
|
||||||
**✅ Full Fix Applied Successfully**
|
|
||||||
This update addresses npm security advisories and ensures dependencies are secure and up-to-date.
|
|
||||||
|
|
||||||
**Changes made:**
|
|
||||||
- Applied `npm audit fix --force` to resolve security vulnerabilities
|
|
||||||
- Updated package-lock.json with security patches
|
|
||||||
- Verified build compatibility with `npm run all`
|
|
||||||
|
|
||||||
**Next steps:**
|
|
||||||
- Review the dependency changes
|
|
||||||
- Verify the hashFiles functionality still works as expected
|
|
||||||
- Merge when ready
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Autogenerated by [NPM Audit Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit.yml)
|
|
||||||
EOF
|
|
||||||
elif [ "$AUDIT_FIX_STATUS" = "build_failed" ]; then
|
|
||||||
cat > pr_body.txt << 'EOF'
|
|
||||||
Automated npm audit fix for security vulnerabilities in hashFiles dependencies.
|
|
||||||
|
|
||||||
**⚠️ Security Fixes Applied - Build Issues Need Manual Resolution**
|
|
||||||
This update applies important security patches but causes build failures that require manual fixes.
|
|
||||||
|
|
||||||
**Changes made:**
|
|
||||||
- Applied `npm audit fix --force` to resolve security vulnerabilities
|
|
||||||
- Updated package-lock.json with security patches
|
|
||||||
|
|
||||||
**⚠️ Build Issues Detected:**
|
|
||||||
The build fails after applying security fixes, likely due to TypeScript compatibility issues with updated `@types/node`.
|
|
||||||
|
|
||||||
**Required Manual Fixes:**
|
|
||||||
1. Review TypeScript compilation errors in the build output
|
|
||||||
2. Update TypeScript configuration if needed
|
|
||||||
3. Consider pinning `@types/node` to a compatible version
|
|
||||||
4. Run `npm run all` locally to verify fixes
|
|
||||||
|
|
||||||
**Next steps:**
|
|
||||||
- **DO NOT merge until build issues are resolved**
|
|
||||||
- Apply manual fixes for TypeScript compatibility
|
|
||||||
- Test the hashFiles functionality still works as expected
|
|
||||||
- Merge when build passes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Autogenerated by [NPM Audit Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit.yml)
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
# Fallback case
|
|
||||||
cat > pr_body.txt << 'EOF'
|
|
||||||
Automated npm audit attempted for security vulnerabilities in hashFiles dependencies.
|
|
||||||
|
|
||||||
**ℹ️ No Changes Applied**
|
|
||||||
No security vulnerabilities were found or no changes were needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Autogenerated by [NPM Audit Fix Workflow](https://github.com/actions/runner/blob/main/.github/workflows/npm-audit.yml)
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create PR
|
|
||||||
gh pr create -B main -H "$branch_name" \
|
|
||||||
--title "chore: npm audit fix for hashFiles dependencies" \
|
|
||||||
--label "dependencies" \
|
|
||||||
--label "dependencies-weekly-check" \
|
|
||||||
--label "dependencies-not-dependabot" \
|
|
||||||
--label "npm" \
|
|
||||||
--label "typescript" \
|
|
||||||
--label "security" \
|
|
||||||
--body-file pr_body.txt
|
|
||||||
else
|
|
||||||
echo "✅ No changes to commit - npm audit fix did not modify any files"
|
|
||||||
fi
|
|
||||||
30
.github/workflows/release.yml
vendored
30
.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@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# 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@v8.0.0
|
uses: actions/github-script@v7.0.1
|
||||||
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@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# Build runner layout
|
# Build runner layout
|
||||||
- name: Build & Layout Release
|
- name: Build & Layout Release
|
||||||
@@ -129,41 +129,41 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# 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@v5
|
uses: actions/download-artifact@v4
|
||||||
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@v5
|
uses: actions/download-artifact@v4
|
||||||
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@v5
|
uses: actions/download-artifact@v4
|
||||||
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@v5
|
uses: actions/download-artifact@v4
|
||||||
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@v5
|
uses: actions/download-artifact@v4
|
||||||
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@v5
|
uses: actions/download-artifact@v4
|
||||||
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@v5
|
uses: actions/download-artifact@v4
|
||||||
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@v8.0.0
|
uses: actions/github-script@v7.0.1
|
||||||
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@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Compute image version
|
- name: Compute image version
|
||||||
id: image
|
id: image
|
||||||
uses: actions/github-script@v8.0.0
|
uses: actions/github-script@v7.0.1
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -338,7 +338,7 @@ jobs:
|
|||||||
org.opencontainers.image.licenses=MIT
|
org.opencontainers.image.licenses=MIT
|
||||||
|
|
||||||
- name: Generate attestation
|
- name: Generate attestation
|
||||||
uses: actions/attest-build-provenance@v3
|
uses: actions/attest-build-provenance@v2
|
||||||
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 }}
|
||||||
|
|||||||
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
stale-issue-message: "This issue is stale because it has been open 365 days with no activity. Remove stale label or comment or this will be closed in 15 days."
|
stale-issue-message: "This issue is stale because it has been open 365 days with no activity. Remove stale label or comment or this will be closed in 15 days."
|
||||||
close-issue-message: "This issue was closed because it has been stalled for 15 days with no activity."
|
close-issue-message: "This issue was closed because it has been stalled for 15 days with no activity."
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
cd src/Misc/expressionFunc/hashFiles && npx lint-staged
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
cd src/Misc/expressionFunc/hashFiles
|
||||||
|
|
||||||
|
npx lint-staged
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
# Runner Dependency Management Process
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document outlines the automated dependency management process for the GitHub Actions Runner, designed to ensure we maintain up-to-date and secure dependencies while providing predictable release cycles.
|
|
||||||
|
|
||||||
## Release Schedule
|
|
||||||
|
|
||||||
- **Monthly Runner Releases**: New runner versions are released monthly
|
|
||||||
- **Weekly Dependency Checks**: Automated workflows check for dependency updates every Monday
|
|
||||||
- **Security Patches**: Critical security vulnerabilities are addressed immediately outside the regular schedule
|
|
||||||
|
|
||||||
## Automated Workflows
|
|
||||||
|
|
||||||
**Note**: These workflows are implemented across separate PRs for easier review and independent deployment. Each workflow includes comprehensive error handling and security-focused vulnerability detection.
|
|
||||||
|
|
||||||
### 1. Foundation Labels
|
|
||||||
|
|
||||||
- **Workflow**: `.github/workflows/setup-labels.yml` (PR #4024)
|
|
||||||
- **Purpose**: Creates consistent dependency labels for all automation workflows
|
|
||||||
- **Labels**: `dependencies`, `security`, `typescript`, `needs-manual-review`
|
|
||||||
- **Prerequisite**: Must be merged before other workflows for proper labeling
|
|
||||||
|
|
||||||
### 2. Node.js Version Updates
|
|
||||||
|
|
||||||
- **Workflow**: `.github/workflows/node-upgrade.yml`
|
|
||||||
- **Schedule**: Mondays at 6:00 AM UTC
|
|
||||||
- **Purpose**: Updates Node.js 20 and 24 versions in `src/Misc/externals.sh`
|
|
||||||
- **Source**: [nodejs.org](https://nodejs.org) and [actions/alpine_nodejs](https://github.com/actions/alpine_nodejs)
|
|
||||||
- **Priority**: First (NPM depends on current Node.js versions)
|
|
||||||
|
|
||||||
### 3. NPM Security Audit
|
|
||||||
|
|
||||||
- **Primary Workflow**: `.github/workflows/npm-audit.yml` ("NPM Audit Fix")
|
|
||||||
- **Schedule**: Mondays at 7:00 AM UTC
|
|
||||||
- **Purpose**: Automated security vulnerability detection and basic fixes
|
|
||||||
- **Location**: `src/Misc/expressionFunc/hashFiles/`
|
|
||||||
- **Features**: npm audit, security patch application, PR creation
|
|
||||||
- **Dependency**: Runs after Node.js updates for optimal compatibility
|
|
||||||
|
|
||||||
- **Fallback Workflow**: `.github/workflows/npm-audit-typescript.yml` ("NPM Audit Fix with TypeScript Auto-Fix")
|
|
||||||
- **Trigger**: Manual dispatch only
|
|
||||||
- **Purpose**: Manual security audit with TypeScript compatibility fixes
|
|
||||||
- **Use Case**: When scheduled workflow fails or needs custom intervention
|
|
||||||
- **Features**: Enhanced TypeScript auto-repair, graduated security response
|
|
||||||
- **How to Use**:
|
|
||||||
1. If the scheduled "NPM Audit Fix" workflow fails, go to Actions tab
|
|
||||||
2. Select "NPM Audit Fix with TypeScript Auto-Fix" workflow
|
|
||||||
3. Click "Run workflow" and optionally specify fix level (auto/manual)
|
|
||||||
4. Review the generated PR for TypeScript compatibility issues
|
|
||||||
|
|
||||||
### 4. .NET SDK Updates
|
|
||||||
|
|
||||||
- **Workflow**: `.github/workflows/dotnet-upgrade.yml`
|
|
||||||
- **Schedule**: Mondays at midnight UTC
|
|
||||||
- **Purpose**: Updates .NET SDK and package versions with build validation
|
|
||||||
- **Features**: Global.json updates, NuGet package management, compatibility checking
|
|
||||||
- **Independence**: Runs independently of Node.js/NPM updates
|
|
||||||
|
|
||||||
### 5. Docker/Buildx Updates
|
|
||||||
|
|
||||||
- **Workflow**: `.github/workflows/docker-buildx-upgrade.yml` ("Docker/Buildx Version Upgrade")
|
|
||||||
- **Schedule**: Mondays at midnight UTC
|
|
||||||
- **Purpose**: Updates Docker and Docker Buildx versions with multi-platform validation
|
|
||||||
- **Features**: Container security scanning, multi-architecture build testing
|
|
||||||
- **Independence**: Runs independently of other dependency updates
|
|
||||||
|
|
||||||
### 6. Dependency Monitoring
|
|
||||||
|
|
||||||
- **Workflow**: `.github/workflows/dependency-check.yml` ("Dependency Status Check")
|
|
||||||
- **Schedule**: Mondays at 11:00 AM UTC
|
|
||||||
- **Purpose**: Comprehensive status report of all dependencies with security audit
|
|
||||||
- **Features**: Multi-dependency checking, npm audit status, build validation, choice of specific component checks
|
|
||||||
- **Summary**: Runs last to capture results from all morning dependency updates
|
|
||||||
|
|
||||||
## Release Process Integration
|
|
||||||
|
|
||||||
### Pre-Release Checklist
|
|
||||||
|
|
||||||
Before each monthly runner release:
|
|
||||||
|
|
||||||
1. **Check Dependency PRs**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List all open dependency PRs
|
|
||||||
gh pr list --label "dependencies" --state open
|
|
||||||
|
|
||||||
# List only automated weekly dependency updates
|
|
||||||
gh pr list --label "dependencies-weekly-check" --state open
|
|
||||||
|
|
||||||
# List only custom dependency automation (not dependabot)
|
|
||||||
gh pr list --label "dependencies-not-dependabot" --state open
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Run Manual Dependency Check**:
|
|
||||||
- Go to Actions tab → "Dependency Status Check" → "Run workflow"
|
|
||||||
- Review the summary for any outdated dependencies
|
|
||||||
|
|
||||||
3. **Review and Merge Updates**:
|
|
||||||
- Prioritize security-related updates
|
|
||||||
- Test dependency updates in development environment
|
|
||||||
- Merge approved dependency PRs
|
|
||||||
|
|
||||||
### Vulnerability Response
|
|
||||||
|
|
||||||
#### Critical Security Vulnerabilities
|
|
||||||
|
|
||||||
- **Response Time**: Within 24 hours
|
|
||||||
- **Process**:
|
|
||||||
1. Assess impact on runner security
|
|
||||||
2. Create hotfix branch if runner data security is affected
|
|
||||||
3. Expedite patch release if necessary
|
|
||||||
4. Document in security advisory if applicable
|
|
||||||
|
|
||||||
#### Non-Critical Vulnerabilities
|
|
||||||
|
|
||||||
- **Response Time**: Next monthly release
|
|
||||||
- **Process**:
|
|
||||||
1. Evaluate if vulnerability affects runner functionality
|
|
||||||
2. Include fix in regular dependency update cycle
|
|
||||||
3. Document in release notes
|
|
||||||
|
|
||||||
## Monitoring and Alerts
|
|
||||||
|
|
||||||
### GitHub Actions Workflow Status
|
|
||||||
|
|
||||||
- All dependency workflows create PRs with the `dependencies` label
|
|
||||||
- Failed workflows should be investigated immediately
|
|
||||||
- Weekly dependency status reports are generated automatically
|
|
||||||
|
|
||||||
### Manual Checks
|
|
||||||
|
|
||||||
You can manually trigger dependency checks:
|
|
||||||
|
|
||||||
- **Full Status**: Run "Dependency Status Check" workflow
|
|
||||||
- **Specific Component**: Use the dropdown to check individual dependencies
|
|
||||||
|
|
||||||
## Dependency Labels
|
|
||||||
|
|
||||||
All automated dependency PRs are tagged with labels for easy filtering and management:
|
|
||||||
|
|
||||||
### Primary Labels
|
|
||||||
|
|
||||||
- **`dependencies`**: All automated dependency-related PRs
|
|
||||||
- **`dependencies-weekly-check`**: Automated weekly dependency updates from scheduled workflows
|
|
||||||
- **`dependencies-not-dependabot`**: Custom dependency automation (not created by dependabot)
|
|
||||||
- **`security`**: Security vulnerability fixes and patches
|
|
||||||
- **`typescript`**: TypeScript compatibility and type definition updates
|
|
||||||
- **`needs-manual-review`**: Complex updates requiring human verification
|
|
||||||
|
|
||||||
### Technology-Specific Labels
|
|
||||||
|
|
||||||
- **`node`**: Node.js version updates
|
|
||||||
- **`javascript`**: JavaScript runtime and tooling updates
|
|
||||||
- **`npm`**: NPM package and security updates
|
|
||||||
- **`dotnet`**: .NET SDK and NuGet package updates
|
|
||||||
- **`docker`**: Docker and container tooling updates
|
|
||||||
|
|
||||||
### Workflow-Specific Branches
|
|
||||||
|
|
||||||
- **Node.js updates**: `chore/update-node` branch
|
|
||||||
- **NPM security fixes**: `chore/npm-audit-fix-YYYYMMDD` and `chore/npm-audit-fix-with-ts-repair` branches
|
|
||||||
- **NuGet/.NET updates**: `feature/dotnetsdk-upgrade/{version}` branches
|
|
||||||
- **Docker updates**: `feature/docker-buildx-upgrade` branch
|
|
||||||
|
|
||||||
## Special Considerations
|
|
||||||
|
|
||||||
### Node.js Updates
|
|
||||||
|
|
||||||
When updating Node.js versions, remember to:
|
|
||||||
|
|
||||||
1. Create a corresponding release in [actions/alpine_nodejs](https://github.com/actions/alpine_nodejs)
|
|
||||||
2. Follow the alpine_nodejs getting started guide
|
|
||||||
3. Test container builds with new Node versions
|
|
||||||
|
|
||||||
### .NET SDK Updates
|
|
||||||
|
|
||||||
- Only patch versions are auto-updated within the same major.minor version
|
|
||||||
- Major/minor version updates require manual review and testing
|
|
||||||
|
|
||||||
### Docker Updates
|
|
||||||
|
|
||||||
- Updates include both Docker Engine and Docker Buildx
|
|
||||||
- Verify compatibility with runner container workflows
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **NPM Audit Workflow Fails**:
|
|
||||||
- Check if `package.json` exists in `src/Misc/expressionFunc/hashFiles/`
|
|
||||||
- Verify Node.js setup step succeeded
|
|
||||||
|
|
||||||
2. **Version Detection Fails**:
|
|
||||||
- Check if upstream APIs are available
|
|
||||||
- Verify parsing logic for version extraction
|
|
||||||
|
|
||||||
3. **PR Creation Fails**:
|
|
||||||
- Ensure `GITHUB_TOKEN` has sufficient permissions
|
|
||||||
- Check if branch already exists
|
|
||||||
|
|
||||||
### Contact
|
|
||||||
|
|
||||||
For questions about the dependency management process:
|
|
||||||
|
|
||||||
- Create an issue with the `dependencies` label
|
|
||||||
- Review existing dependency management workflows
|
|
||||||
- Consult the runner team for security-related concerns
|
|
||||||
|
|
||||||
## Metrics and KPIs
|
|
||||||
|
|
||||||
Track these metrics to measure dependency management effectiveness:
|
|
||||||
|
|
||||||
- Number of open dependency PRs at release time
|
|
||||||
- Time to merge dependency updates
|
|
||||||
- Number of security vulnerabilities by severity
|
|
||||||
- Release cycle adherence (monthly target)
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Supported Distributions and Versions
|
## Supported Distributions and Versions
|
||||||
|
|
||||||
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/reference/runners/self-hosted-runners#linux)."
|
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#linux)."
|
||||||
|
|
||||||
## Install .Net Core 3.x Linux Dependencies
|
## Install .Net Core 3.x Linux Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/reference/runners/self-hosted-runners#macos)."
|
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#macos)."
|
||||||
|
|
||||||
## [More .Net Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/macos-prerequisites?tabs=netcore30)
|
## [More .Net Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/macos-prerequisites?tabs=netcore30)
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/reference/runners/self-hosted-runners#windows)."
|
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#windows)."
|
||||||
|
|
||||||
## [More .NET Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore30)
|
## [More .NET Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore30)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ ARG TARGETOS
|
|||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RUNNER_VERSION
|
ARG RUNNER_VERSION
|
||||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||||
ARG DOCKER_VERSION=28.5.1
|
ARG DOCKER_VERSION=28.3.0
|
||||||
ARG BUILDX_VERSION=0.29.1
|
ARG BUILDX_VERSION=0.25.0
|
||||||
|
|
||||||
RUN apt update -y && apt install curl unzip -y
|
RUN apt update -y && apt install curl unzip -y
|
||||||
|
|
||||||
@@ -21,10 +21,6 @@ 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.0/actions-runner-hooks-k8s-0.8.0.zip \
|
|
||||||
&& unzip ./runner-container-hooks.zip -d ./k8s-novolume \
|
|
||||||
&& rm runner-container-hooks.zip
|
|
||||||
|
|
||||||
RUN export RUNNER_ARCH=${TARGETARCH} \
|
RUN export RUNNER_ARCH=${TARGETARCH} \
|
||||||
&& if [ "$RUNNER_ARCH" = "amd64" ]; then export DOCKER_ARCH=x86_64 ; fi \
|
&& if [ "$RUNNER_ARCH" = "amd64" ]; then export DOCKER_ARCH=x86_64 ; fi \
|
||||||
&& if [ "$RUNNER_ARCH" = "arm64" ]; then export DOCKER_ARCH=aarch64 ; fi \
|
&& if [ "$RUNNER_ARCH" = "arm64" ]; then export DOCKER_ARCH=aarch64 ; fi \
|
||||||
|
|||||||
@@ -1,43 +1,13 @@
|
|||||||
## What's Changed
|
## What's Changed
|
||||||
* Update safe_sleep.sh for bug when scheduler is paused for more than 1 second by @horner in https://github.com/actions/runner/pull/3157
|
* Try add orchestrationid into user-agent using token claim. by @TingluoHuang in https://github.com/actions/runner/pull/3945
|
||||||
* Acknowledge runner request by @ericsciple in https://github.com/actions/runner/pull/3996
|
* Fix null reference exception in user agent handling by @salmanmkc in https://github.com/actions/runner/pull/3946
|
||||||
* Update Docker to v28.3.3 and Buildx to v0.27.0 by @github-actions[bot] in https://github.com/actions/runner/pull/3999
|
* Runner Support for executing Node24 Actions by @salmanmkc in https://github.com/actions/runner/pull/3940
|
||||||
* Update dotnet sdk to latest version @8.0.413 by @github-actions[bot] in https://github.com/actions/runner/pull/4000
|
* Update dotnet sdk to latest version @8.0.412 by @github-actions[bot] in https://github.com/actions/runner/pull/3941
|
||||||
* Bump actions/attest-build-provenance from 2 to 3 by @dependabot[bot] in https://github.com/actions/runner/pull/4002
|
|
||||||
* Bump @typescript-eslint/eslint-plugin from 6.7.2 to 8.35.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/3920
|
|
||||||
* Bump husky from 8.0.3 to 9.1.7 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/3842
|
|
||||||
* Bump @vercel/ncc from 0.38.0 to 0.38.3 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/3841
|
|
||||||
* Bump eslint-plugin-github from 4.10.0 to 4.10.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/3180
|
|
||||||
* Bump typescript from 5.2.2 to 5.9.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4007
|
|
||||||
* chore: migrate Husky config from v8 to v9 format by @salmanmkc in https://github.com/actions/runner/pull/4003
|
|
||||||
* Map RUNNER_TEMP for container action by @ericsciple in https://github.com/actions/runner/pull/4011
|
|
||||||
* Break UseV2Flow into UseV2Flow and UseRunnerAdminFlow. by @TingluoHuang in https://github.com/actions/runner/pull/4013
|
|
||||||
* Update Docker to v28.4.0 and Buildx to v0.28.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4020
|
|
||||||
* Bump node.js to latest version in runner. by @TingluoHuang in https://github.com/actions/runner/pull/4022
|
|
||||||
* feat: add automated .NET dependency management workflow by @salmanmkc in https://github.com/actions/runner/pull/4028
|
|
||||||
* feat: add automated Docker BuildX dependency management workflow by @salmanmkc in https://github.com/actions/runner/pull/4029
|
|
||||||
* feat: add automated Node.js version management workflow by @salmanmkc in https://github.com/actions/runner/pull/4026
|
|
||||||
* feat: add comprehensive NPM security management workflow by @salmanmkc in https://github.com/actions/runner/pull/4027
|
|
||||||
* feat: add comprehensive dependency monitoring system by @salmanmkc in https://github.com/actions/runner/pull/4025
|
|
||||||
* Use BrokerURL when using RunnerAdmin by @luketomlinson in https://github.com/actions/runner/pull/4044
|
|
||||||
* Bump actions/github-script from 7.0.1 to 8.0.0 by @dependabot[bot] in https://github.com/actions/runner/pull/4016
|
|
||||||
* Bump actions/stale from 9 to 10 by @dependabot[bot] in https://github.com/actions/runner/pull/4015
|
|
||||||
* fix: prevent Node.js upgrade workflow from creating PRs with empty versions by @salmanmkc in https://github.com/actions/runner/pull/4055
|
|
||||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4057
|
|
||||||
* Bump actions/setup-node from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/4037
|
|
||||||
* Bump Azure.Storage.Blobs from 12.25.0 to 12.25.1 by @dependabot[bot] in https://github.com/actions/runner/pull/4058
|
|
||||||
* Update Docker to v28.5.0 and Buildx to v0.29.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4069
|
|
||||||
* Bump github/codeql-action from 3 to 4 by @dependabot[bot] in https://github.com/actions/runner/pull/4072
|
|
||||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4075
|
|
||||||
* Include k8s novolume (version v0.8.0) by @nikola-jokic in https://github.com/actions/runner/pull/4063
|
|
||||||
* Make sure runner-admin has both auth_url and auth_url_v2. by @TingluoHuang in https://github.com/actions/runner/pull/4066
|
|
||||||
* Report job has infra failure to run-service by @TingluoHuang in https://github.com/actions/runner/pull/4073
|
|
||||||
* Bump actions/setup-node from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4078
|
|
||||||
|
|
||||||
## New Contributors
|
## New Contributors
|
||||||
* @horner made their first contribution in https://github.com/actions/runner/pull/3157
|
* @salmanmkc made their first contribution in https://github.com/actions/runner/pull/3946
|
||||||
|
|
||||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.328.0...v2.329.0
|
**Full Changelog**: https://github.com/actions/runner/compare/v2.326.0...v2.327.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.327.1
|
||||||
|
|||||||
1085
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
1085
src/Misc/expressionFunc/hashFiles/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
|||||||
"lint": "eslint src/**/*.ts",
|
"lint": "eslint src/**/*.ts",
|
||||||
"pack": "ncc build -o ../../layoutbin/hashFiles",
|
"pack": "ncc build -o ../../layoutbin/hashFiles",
|
||||||
"all": "npm run format && npm run lint && npm run build && npm run pack",
|
"all": "npm run format && npm run lint && npm run build && npm run pack",
|
||||||
"prepare": "cd ../../../../ && husky"
|
"prepare": "cd ../../../../ && husky install"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -36,15 +36,15 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.6.2",
|
"@types/node": "^20.6.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||||
"@typescript-eslint/parser": "^6.7.2",
|
"@typescript-eslint/parser": "^6.7.2",
|
||||||
"@vercel/ncc": "^0.38.3",
|
"@vercel/ncc": "^0.38.0",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-plugin-github": "^4.10.2",
|
"eslint-plugin-github": "^4.10.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^15.5.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.2.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.19.5"
|
NODE20_VERSION="20.19.3"
|
||||||
NODE24_VERSION="24.11.0"
|
NODE24_VERSION="24.4.0"
|
||||||
|
|
||||||
get_abs_path() {
|
get_abs_path() {
|
||||||
# exploits the fact that pwd will print abs path when no args
|
# exploits the fact that pwd will print abs path when no args
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
while [[ $SECONDS -lt $1 ]]; do
|
while [[ $SECONDS != $1 ]]; do
|
||||||
:
|
:
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
Task<TaskAgentMessage> GetRunnerMessageAsync(Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate, CancellationToken token);
|
Task<TaskAgentMessage> GetRunnerMessageAsync(Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, bool disableUpdate, CancellationToken token);
|
||||||
|
|
||||||
Task AcknowledgeRunnerRequestAsync(string runnerRequestId, Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, CancellationToken token);
|
|
||||||
|
|
||||||
Task UpdateConnectionIfNeeded(Uri serverUri, VssCredentials credentials);
|
Task UpdateConnectionIfNeeded(Uri serverUri, VssCredentials credentials);
|
||||||
|
|
||||||
Task ForceRefreshConnection(VssCredentials credentials);
|
Task ForceRefreshConnection(VssCredentials credentials);
|
||||||
@@ -69,17 +67,10 @@ namespace GitHub.Runner.Common
|
|||||||
var brokerSession = RetryRequest<TaskAgentMessage>(
|
var brokerSession = RetryRequest<TaskAgentMessage>(
|
||||||
async () => await _brokerHttpClient.GetRunnerMessageAsync(sessionId, version, status, os, architecture, disableUpdate, cancellationToken), cancellationToken, shouldRetry: ShouldRetryException);
|
async () => await _brokerHttpClient.GetRunnerMessageAsync(sessionId, version, status, os, architecture, disableUpdate, cancellationToken), cancellationToken, shouldRetry: ShouldRetryException);
|
||||||
|
|
||||||
|
|
||||||
return brokerSession;
|
return brokerSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AcknowledgeRunnerRequestAsync(string runnerRequestId, Guid? sessionId, TaskAgentStatus status, string version, string os, string architecture, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
CheckConnection();
|
|
||||||
|
|
||||||
// No retries
|
|
||||||
await _brokerHttpClient.AcknowledgeRunnerRequestAsync(runnerRequestId, sessionId, version, status, os, architecture, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DeleteSessionAsync(CancellationToken cancellationToken)
|
public async Task DeleteSessionAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using GitHub.Runner.Sdk;
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
namespace GitHub.Runner.Common
|
||||||
{
|
{
|
||||||
@@ -53,9 +53,6 @@ namespace GitHub.Runner.Common
|
|||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public bool UseV2Flow { get; set; }
|
public bool UseV2Flow { get; set; }
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
|
||||||
public bool UseRunnerAdminFlow { get; set; }
|
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public string ServerUrlV2 { get; set; }
|
public string ServerUrlV2 { get; set; }
|
||||||
|
|
||||||
@@ -64,20 +61,8 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
// If the value has been explicitly set, return it.
|
// Old runners do not have this property. Hosted runners likely don't have this property either.
|
||||||
if (_isHostedServer.HasValue)
|
return _isHostedServer ?? true;
|
||||||
{
|
|
||||||
return _isHostedServer.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, try to infer it from the GitHubUrl.
|
|
||||||
if (!string.IsNullOrEmpty(GitHubUrl))
|
|
||||||
{
|
|
||||||
return UrlUtil.IsHostedServer(new UriBuilder(GitHubUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to true since Hosted runners likely don't have this property set.
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set
|
set
|
||||||
|
|||||||
@@ -169,26 +169,6 @@ 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 SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
|
|
||||||
public static readonly string CompareTemplateEvaluator = "actions_runner_compare_template_evaluator";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node version migration related constants
|
|
||||||
public static class NodeMigration
|
|
||||||
{
|
|
||||||
// Node versions
|
|
||||||
public static readonly string Node20 = "node20";
|
|
||||||
public static readonly string Node24 = "node24";
|
|
||||||
|
|
||||||
// Environment variables for controlling node version selection
|
|
||||||
public static readonly string ForceNode24Variable = "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24";
|
|
||||||
public static readonly string AllowUnsecureNodeVersionVariable = "ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION";
|
|
||||||
|
|
||||||
// Feature flags for controlling the migration phases
|
|
||||||
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
|
|
||||||
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ namespace GitHub.Runner.Common
|
|||||||
string environmentUrl,
|
string environmentUrl,
|
||||||
IList<Telemetry> telemetry,
|
IList<Telemetry> telemetry,
|
||||||
string billingOwnerId,
|
string billingOwnerId,
|
||||||
string infrastructureFailureCategory,
|
|
||||||
CancellationToken token);
|
CancellationToken token);
|
||||||
|
|
||||||
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
||||||
@@ -81,12 +80,11 @@ namespace GitHub.Runner.Common
|
|||||||
string environmentUrl,
|
string environmentUrl,
|
||||||
IList<Telemetry> telemetry,
|
IList<Telemetry> telemetry,
|
||||||
string billingOwnerId,
|
string billingOwnerId,
|
||||||
string infrastructureFailureCategory,
|
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return RetryRequest(
|
return RetryRequest(
|
||||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, environmentUrl, telemetry, billingOwnerId, infrastructureFailureCategory, cancellationToken), cancellationToken,
|
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, environmentUrl, telemetry, billingOwnerId, cancellationToken), cancellationToken,
|
||||||
shouldRetry: ex =>
|
shouldRetry: ex =>
|
||||||
ex is not VssUnauthorizedException && // HTTP status 401
|
ex is not VssUnauthorizedException && // HTTP status 401
|
||||||
ex is not TaskOrchestrationJobNotFoundException); // HTTP status 404
|
ex is not TaskOrchestrationJobNotFoundException); // HTTP status 404
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
Task<DistributedTask.WebApi.Runner> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey);
|
Task<DistributedTask.WebApi.Runner> AddRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey);
|
||||||
Task<DistributedTask.WebApi.Runner> ReplaceRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey);
|
Task<DistributedTask.WebApi.Runner> ReplaceRunnerAsync(int runnerGroupId, TaskAgent agent, string githubUrl, string githubToken, string publicKey);
|
||||||
Task DeleteRunnerAsync(string githubUrl, string githubToken, ulong runnerId);
|
|
||||||
Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken);
|
Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,15 +43,117 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
public async Task<List<TaskAgent>> GetRunnerByNameAsync(string githubUrl, string githubToken, string agentName)
|
public async Task<List<TaskAgent>> GetRunnerByNameAsync(string githubUrl, string githubToken, string agentName)
|
||||||
{
|
{
|
||||||
var githubApiUrl = $"{GetEntityUrl(githubUrl)}/runners?name={Uri.EscapeDataString(agentName)}";
|
var githubApiUrl = "";
|
||||||
|
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
||||||
|
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var isOrgRunner = path.Length == 1;
|
||||||
|
var isRepoOrEnterpriseRunner = path.Length == 2;
|
||||||
|
var isRepoRunner = isRepoOrEnterpriseRunner && !string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isOrgRunner)
|
||||||
|
{
|
||||||
|
// org runner
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isRepoOrEnterpriseRunner)
|
||||||
|
{
|
||||||
|
// Repository runner
|
||||||
|
if (isRepoRunner)
|
||||||
|
{
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/repos/{path[0]}/{path[1]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/repos/{path[0]}/{path[1]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Enterprise runner
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions/runners?name={Uri.EscapeDataString(agentName)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
|
||||||
|
}
|
||||||
|
|
||||||
var runnersList = await RetryRequest<ListRunnersResponse>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
var runnersList = await RetryRequest<ListRunnersResponse>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
||||||
|
|
||||||
return runnersList.ToTaskAgents();
|
return runnersList.ToTaskAgents();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken)
|
public async Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken)
|
||||||
{
|
{
|
||||||
var githubApiUrl = $"{GetEntityUrl(githubUrl)}/runner-groups";
|
var githubApiUrl = "";
|
||||||
|
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
||||||
|
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var isOrgRunner = path.Length == 1;
|
||||||
|
var isRepoOrEnterpriseRunner = path.Length == 2;
|
||||||
|
var isRepoRunner = isRepoOrEnterpriseRunner && !string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isOrgRunner)
|
||||||
|
{
|
||||||
|
// org runner
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isRepoOrEnterpriseRunner)
|
||||||
|
{
|
||||||
|
// Repository Runner
|
||||||
|
if (isRepoRunner)
|
||||||
|
{
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/repos/{path[0]}/{path[1]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/repos/{path[0]}/{path[1]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Enterprise Runner
|
||||||
|
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions/runner-groups";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
|
||||||
|
}
|
||||||
|
|
||||||
var agentPools = await RetryRequest<RunnerGroupList>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
var agentPools = await RetryRequest<RunnerGroupList>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
|
||||||
|
|
||||||
return agentPools?.ToAgentPoolList();
|
return agentPools?.ToAgentPoolList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +204,6 @@ namespace GitHub.Runner.Common
|
|||||||
return await RetryRequest<DistributedTask.WebApi.Runner>(githubApiUrl, githubToken, RequestType.Post, 3, "Failed to add agent", body);
|
return await RetryRequest<DistributedTask.WebApi.Runner>(githubApiUrl, githubToken, RequestType.Post, 3, "Failed to add agent", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteRunnerAsync(string githubUrl, string githubToken, ulong runnerId)
|
|
||||||
{
|
|
||||||
var githubApiUrl = $"{GetEntityUrl(githubUrl)}/runners/{runnerId}";
|
|
||||||
await RetryRequest<DistributedTask.WebApi.Runner>(githubApiUrl, githubToken, RequestType.Delete, 3, "Failed to delete agent");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<T> RetryRequest<T>(string githubApiUrl, string githubToken, RequestType requestType, int maxRetryAttemptsCount = 5, string errorMessage = null, StringContent body = null)
|
private async Task<T> RetryRequest<T>(string githubApiUrl, string githubToken, RequestType requestType, int maxRetryAttemptsCount = 5, string errorMessage = null, StringContent body = null)
|
||||||
{
|
{
|
||||||
int retry = 0;
|
int retry = 0;
|
||||||
@@ -125,22 +220,13 @@ namespace GitHub.Runner.Common
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpResponseMessage response = null;
|
HttpResponseMessage response = null;
|
||||||
switch (requestType)
|
if (requestType == RequestType.Get)
|
||||||
{
|
{
|
||||||
case RequestType.Get:
|
response = await httpClient.GetAsync(githubApiUrl);
|
||||||
response = await httpClient.GetAsync(githubApiUrl);
|
}
|
||||||
break;
|
else
|
||||||
case RequestType.Post:
|
{
|
||||||
response = await httpClient.PostAsync(githubApiUrl, body);
|
response = await httpClient.PostAsync(githubApiUrl, body);
|
||||||
break;
|
|
||||||
case RequestType.Patch:
|
|
||||||
response = await httpClient.PatchAsync(githubApiUrl, body);
|
|
||||||
break;
|
|
||||||
case RequestType.Delete:
|
|
||||||
response = await httpClient.DeleteAsync(githubApiUrl);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(requestType), requestType, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response != null)
|
if (response != null)
|
||||||
@@ -175,61 +261,5 @@ namespace GitHub.Runner.Common
|
|||||||
await Task.Delay(backOff);
|
await Task.Delay(backOff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetEntityUrl(string githubUrl)
|
|
||||||
{
|
|
||||||
var githubApiUrl = "";
|
|
||||||
var gitHubUrlBuilder = new UriBuilder(githubUrl);
|
|
||||||
var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
var isOrgRunner = path.Length == 1;
|
|
||||||
var isRepoOrEnterpriseRunner = path.Length == 2;
|
|
||||||
var isRepoRunner = isRepoOrEnterpriseRunner && !string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (isOrgRunner)
|
|
||||||
{
|
|
||||||
// org runner
|
|
||||||
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
|
||||||
{
|
|
||||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (isRepoOrEnterpriseRunner)
|
|
||||||
{
|
|
||||||
// Repository Runner
|
|
||||||
if (isRepoRunner)
|
|
||||||
{
|
|
||||||
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
|
||||||
{
|
|
||||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/repos/{path[0]}/{path[1]}/actions";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/repos/{path[0]}/{path[1]}/actions";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Enterprise Runner
|
|
||||||
if (UrlUtil.IsHostedServer(gitHubUrlBuilder))
|
|
||||||
{
|
|
||||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{path[0]}/{path[1]}/actions";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{path[0]}/{path[1]}/actions";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"'{githubUrl}' should point to an org or enterprise.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return githubApiUrl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
protected async Task RetryRequest(Func<Task> func,
|
protected async Task RetryRequest(Func<Task> func,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
int maxAttempts = 5,
|
int maxRetryAttemptsCount = 5,
|
||||||
Func<Exception, bool> shouldRetry = null
|
Func<Exception, bool> shouldRetry = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -79,31 +79,31 @@ namespace GitHub.Runner.Common
|
|||||||
await func();
|
await func();
|
||||||
return Unit.Value;
|
return Unit.Value;
|
||||||
}
|
}
|
||||||
await RetryRequest<Unit>(wrappedFunc, cancellationToken, maxAttempts, shouldRetry);
|
await RetryRequest<Unit>(wrappedFunc, cancellationToken, maxRetryAttemptsCount, shouldRetry);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task<T> RetryRequest<T>(Func<Task<T>> func,
|
protected async Task<T> RetryRequest<T>(Func<Task<T>> func,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
int maxAttempts = 5,
|
int maxRetryAttemptsCount = 5,
|
||||||
Func<Exception, bool> shouldRetry = null
|
Func<Exception, bool> shouldRetry = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var attempt = 0;
|
var retryCount = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
attempt++;
|
retryCount++;
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await func();
|
return await func();
|
||||||
}
|
}
|
||||||
// TODO: Add handling of non-retriable exceptions: https://github.com/github/actions-broker/issues/122
|
// TODO: Add handling of non-retriable exceptions: https://github.com/github/actions-broker/issues/122
|
||||||
catch (Exception ex) when (attempt < maxAttempts && (shouldRetry == null || shouldRetry(ex)))
|
catch (Exception ex) when (retryCount < maxRetryAttemptsCount && (shouldRetry == null || shouldRetry(ex)))
|
||||||
{
|
{
|
||||||
Trace.Error("Catch exception during request");
|
Trace.Error("Catch exception during request");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
|
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
|
||||||
Trace.Warning($"Back off {backOff.TotalSeconds} seconds before next retry. {maxAttempts - attempt} attempt left.");
|
Trace.Warning($"Back off {backOff.TotalSeconds} seconds before next retry. {maxRetryAttemptsCount - retryCount} attempt left.");
|
||||||
await Task.Delay(backOff, cancellationToken);
|
await Task.Delay(backOff, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Util
|
namespace GitHub.Runner.Common.Util
|
||||||
{
|
{
|
||||||
public static class NodeUtil
|
public static class NodeUtil
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Represents details about an environment variable, including its value and source
|
|
||||||
/// </summary>
|
|
||||||
private class EnvironmentVariableInfo
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the value evaluates to true
|
|
||||||
/// </summary>
|
|
||||||
public bool IsTrue { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the value came from the workflow environment
|
|
||||||
/// </summary>
|
|
||||||
public bool FromWorkflow { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether the value came from the system environment
|
|
||||||
/// </summary>
|
|
||||||
public bool FromSystem { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string _defaultNodeVersion = "node20";
|
private const string _defaultNodeVersion = "node20";
|
||||||
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] { "node20" });
|
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] { "node20" });
|
||||||
public static string GetInternalNodeVersion()
|
public static string GetInternalNodeVersion()
|
||||||
@@ -41,70 +18,6 @@ namespace GitHub.Runner.Common.Util
|
|||||||
}
|
}
|
||||||
return _defaultNodeVersion;
|
return _defaultNodeVersion;
|
||||||
}
|
}
|
||||||
/// <summary>
|
|
||||||
/// Determines the appropriate Node version for Actions to use
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="workflowEnvironment">Optional dictionary containing workflow-level environment variables</param>
|
|
||||||
/// <param name="useNode24ByDefault">Feature flag indicating if Node 24 should be the default</param>
|
|
||||||
/// <param name="requireNode24">Feature flag indicating if Node 24 is required</param>
|
|
||||||
/// <returns>The Node version to use (node20 or node24) and warning message if both env vars are set</returns>
|
|
||||||
public static (string nodeVersion, string warningMessage) DetermineActionsNodeVersion(
|
|
||||||
IDictionary<string, string> workflowEnvironment = null,
|
|
||||||
bool useNode24ByDefault = false,
|
|
||||||
bool requireNode24 = false)
|
|
||||||
{
|
|
||||||
// Phase 3: Always use Node 24 regardless of environment variables
|
|
||||||
if (requireNode24)
|
|
||||||
{
|
|
||||||
return (Constants.Runner.NodeMigration.Node24, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get environment variable details with source information
|
|
||||||
var forceNode24Details = GetEnvironmentVariableDetails(
|
|
||||||
Constants.Runner.NodeMigration.ForceNode24Variable, workflowEnvironment);
|
|
||||||
|
|
||||||
var allowUnsecureNodeDetails = GetEnvironmentVariableDetails(
|
|
||||||
Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, workflowEnvironment);
|
|
||||||
|
|
||||||
bool forceNode24 = forceNode24Details.IsTrue;
|
|
||||||
bool allowUnsecureNode = allowUnsecureNodeDetails.IsTrue;
|
|
||||||
string warningMessage = null;
|
|
||||||
|
|
||||||
// Check if both flags are set from the same source
|
|
||||||
bool bothFromWorkflow = forceNode24Details.IsTrue && allowUnsecureNodeDetails.IsTrue &&
|
|
||||||
forceNode24Details.FromWorkflow && allowUnsecureNodeDetails.FromWorkflow;
|
|
||||||
|
|
||||||
bool bothFromSystem = forceNode24Details.IsTrue && allowUnsecureNodeDetails.IsTrue &&
|
|
||||||
forceNode24Details.FromSystem && allowUnsecureNodeDetails.FromSystem;
|
|
||||||
|
|
||||||
// Handle the case when both are set in the same source
|
|
||||||
if (bothFromWorkflow || bothFromSystem)
|
|
||||||
{
|
|
||||||
string source = bothFromWorkflow ? "workflow" : "system";
|
|
||||||
string defaultVersion = useNode24ByDefault ? Constants.Runner.NodeMigration.Node24 : Constants.Runner.NodeMigration.Node20;
|
|
||||||
warningMessage = $"Both {Constants.Runner.NodeMigration.ForceNode24Variable} and {Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable} environment variables are set to true in the {source} environment. This is likely a configuration error. Using the default Node version: {defaultVersion}.";
|
|
||||||
return (defaultVersion, warningMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: Node 24 is the default
|
|
||||||
if (useNode24ByDefault)
|
|
||||||
{
|
|
||||||
if (allowUnsecureNode)
|
|
||||||
{
|
|
||||||
return (Constants.Runner.NodeMigration.Node20, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (Constants.Runner.NodeMigration.Node24, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 1: Node 20 is the default
|
|
||||||
if (forceNode24)
|
|
||||||
{
|
|
||||||
return (Constants.Runner.NodeMigration.Node24, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (Constants.Runner.NodeMigration.Node20, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.
|
||||||
@@ -113,50 +26,14 @@ namespace GitHub.Runner.Common.Util
|
|||||||
/// <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(string preferredVersion)
|
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
|
||||||
{
|
{
|
||||||
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) &&
|
if (string.Equals(preferredVersion, "node24", StringComparison.OrdinalIgnoreCase) &&
|
||||||
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
|
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
|
||||||
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
|
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
|
||||||
{
|
{
|
||||||
return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
|
return ("node20", "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (preferredVersion, null);
|
return (preferredVersion, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets detailed information about an environment variable from both workflow and system environments
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="variableName">The name of the environment variable</param>
|
|
||||||
/// <param name="workflowEnvironment">Optional dictionary containing workflow-level environment variables</param>
|
|
||||||
/// <returns>An EnvironmentVariableInfo object containing details about the variable from both sources</returns>
|
|
||||||
private static EnvironmentVariableInfo GetEnvironmentVariableDetails(string variableName, IDictionary<string, string> workflowEnvironment)
|
|
||||||
{
|
|
||||||
var info = new EnvironmentVariableInfo();
|
|
||||||
|
|
||||||
// Check workflow environment
|
|
||||||
bool foundInWorkflow = false;
|
|
||||||
string workflowValue = null;
|
|
||||||
|
|
||||||
if (workflowEnvironment != null && workflowEnvironment.TryGetValue(variableName, out workflowValue))
|
|
||||||
{
|
|
||||||
foundInWorkflow = true;
|
|
||||||
info.FromWorkflow = true;
|
|
||||||
info.IsTrue = StringUtil.ConvertToBoolean(workflowValue); // Workflow value takes precedence for the boolean value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check system environment
|
|
||||||
string systemValue = Environment.GetEnvironmentVariable(variableName);
|
|
||||||
bool foundInSystem = !string.IsNullOrEmpty(systemValue);
|
|
||||||
|
|
||||||
info.FromSystem = foundInSystem;
|
|
||||||
|
|
||||||
// If not found in workflow, use system values
|
|
||||||
if (!foundInWorkflow)
|
|
||||||
{
|
|
||||||
info.IsTrue = StringUtil.ConvertToBoolean(systemValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace GitHub.Runner.Listener
|
|||||||
private RunnerSettings _settings;
|
private RunnerSettings _settings;
|
||||||
private ITerminal _term;
|
private ITerminal _term;
|
||||||
private TimeSpan _getNextMessageRetryInterval;
|
private TimeSpan _getNextMessageRetryInterval;
|
||||||
private TaskAgentStatus _runnerStatus = TaskAgentStatus.Online;
|
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
|
||||||
private CancellationTokenSource _getMessagesTokenSource;
|
private CancellationTokenSource _getMessagesTokenSource;
|
||||||
private VssCredentials _creds;
|
private VssCredentials _creds;
|
||||||
private VssCredentials _credsV2;
|
private VssCredentials _credsV2;
|
||||||
@@ -258,7 +258,7 @@ namespace GitHub.Runner.Listener
|
|||||||
public void OnJobStatus(object sender, JobStatusEventArgs e)
|
public void OnJobStatus(object sender, JobStatusEventArgs e)
|
||||||
{
|
{
|
||||||
Trace.Info("Received job status event. JobState: {0}", e.Status);
|
Trace.Info("Received job status event. JobState: {0}", e.Status);
|
||||||
_runnerStatus = e.Status;
|
runnerStatus = e.Status;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_getMessagesTokenSource?.Cancel();
|
_getMessagesTokenSource?.Cancel();
|
||||||
@@ -291,7 +291,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
|
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
|
||||||
_runnerStatus,
|
runnerStatus,
|
||||||
BuildConstants.RunnerPackage.Version,
|
BuildConstants.RunnerPackage.Version,
|
||||||
VarUtil.OS,
|
VarUtil.OS,
|
||||||
VarUtil.OSArchitecture,
|
VarUtil.OSArchitecture,
|
||||||
@@ -417,21 +417,6 @@ namespace GitHub.Runner.Listener
|
|||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AcknowledgeMessageAsync(string runnerRequestId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Short timeout
|
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
|
||||||
Trace.Info($"Acknowledging runner request '{runnerRequestId}'.");
|
|
||||||
await _brokerServer.AcknowledgeRunnerRequestAsync(
|
|
||||||
runnerRequestId,
|
|
||||||
_session.SessionId,
|
|
||||||
_runnerStatus,
|
|
||||||
BuildConstants.RunnerPackage.Version,
|
|
||||||
VarUtil.OS,
|
|
||||||
VarUtil.OSArchitecture,
|
|
||||||
linkedCts.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsGetNextMessageExceptionRetriable(Exception ex)
|
private bool IsGetNextMessageExceptionRetriable(Exception ex)
|
||||||
{
|
{
|
||||||
if (ex is TaskAgentNotFoundException ||
|
if (ex is TaskAgentNotFoundException ||
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration");
|
registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration");
|
||||||
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register);
|
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register);
|
||||||
runnerSettings.ServerUrl = authResult.TenantUrl;
|
runnerSettings.ServerUrl = authResult.TenantUrl;
|
||||||
runnerSettings.UseRunnerAdminFlow = authResult.UseRunnerAdminFlow;
|
runnerSettings.UseV2Flow = authResult.UseV2Flow;
|
||||||
Trace.Info($"Using runner-admin flow: {runnerSettings.UseRunnerAdminFlow}");
|
Trace.Info($"Using V2 flow: {runnerSettings.UseV2Flow}");
|
||||||
creds = authResult.ToVssCredentials();
|
creds = authResult.ToVssCredentials();
|
||||||
Trace.Info("cred retrieved via GitHub auth");
|
Trace.Info("cred retrieved via GitHub auth");
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
string poolName = null;
|
string poolName = null;
|
||||||
TaskAgentPool agentPool = null;
|
TaskAgentPool agentPool = null;
|
||||||
List<TaskAgentPool> agentPools;
|
List<TaskAgentPool> agentPools;
|
||||||
if (runnerSettings.UseRunnerAdminFlow)
|
if (runnerSettings.UseV2Flow)
|
||||||
{
|
{
|
||||||
agentPools = await _dotcomServer.GetRunnerGroupsAsync(runnerSettings.GitHubUrl, registerToken);
|
agentPools = await _dotcomServer.GetRunnerGroupsAsync(runnerSettings.GitHubUrl, registerToken);
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
var userLabels = command.GetLabels();
|
var userLabels = command.GetLabels();
|
||||||
_term.WriteLine();
|
_term.WriteLine();
|
||||||
List<TaskAgent> agents;
|
List<TaskAgent> agents;
|
||||||
if (runnerSettings.UseRunnerAdminFlow)
|
if (runnerSettings.UseV2Flow)
|
||||||
{
|
{
|
||||||
agents = await _dotcomServer.GetRunnerByNameAsync(runnerSettings.GitHubUrl, registerToken, runnerSettings.AgentName);
|
agents = await _dotcomServer.GetRunnerByNameAsync(runnerSettings.GitHubUrl, registerToken, runnerSettings.AgentName);
|
||||||
}
|
}
|
||||||
@@ -280,11 +280,10 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (runnerSettings.UseRunnerAdminFlow)
|
if (runnerSettings.UseV2Flow)
|
||||||
{
|
{
|
||||||
var runner = await _dotcomServer.ReplaceRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
|
var runner = await _dotcomServer.ReplaceRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
|
||||||
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
|
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
|
||||||
runnerSettings.UseV2Flow = true; // if we are using runner admin, we also need to hit broker
|
|
||||||
|
|
||||||
agent.Id = runner.Id;
|
agent.Id = runner.Id;
|
||||||
agent.Authorization = new TaskAgentAuthorization()
|
agent.Authorization = new TaskAgentAuthorization()
|
||||||
@@ -292,13 +291,6 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
AuthorizationUrl = runner.RunnerAuthorization.AuthorizationUrl,
|
AuthorizationUrl = runner.RunnerAuthorization.AuthorizationUrl,
|
||||||
ClientId = new Guid(runner.RunnerAuthorization.ClientId)
|
ClientId = new Guid(runner.RunnerAuthorization.ClientId)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(runner.RunnerAuthorization.LegacyAuthorizationUrl?.AbsoluteUri))
|
|
||||||
{
|
|
||||||
agent.Authorization.AuthorizationUrl = runner.RunnerAuthorization.LegacyAuthorizationUrl;
|
|
||||||
agent.Properties["EnableAuthMigrationByDefault"] = true;
|
|
||||||
agent.Properties["AuthorizationUrlV2"] = runner.RunnerAuthorization.AuthorizationUrl.AbsoluteUri;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -338,11 +330,10 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (runnerSettings.UseRunnerAdminFlow)
|
if (runnerSettings.UseV2Flow)
|
||||||
{
|
{
|
||||||
var runner = await _dotcomServer.AddRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
|
var runner = await _dotcomServer.AddRunnerAsync(runnerSettings.PoolId, agent, runnerSettings.GitHubUrl, registerToken, publicKeyXML);
|
||||||
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
|
runnerSettings.ServerUrlV2 = runner.RunnerAuthorization.ServerUrl;
|
||||||
runnerSettings.UseV2Flow = true; // if we are using runner admin, we also need to hit broker
|
|
||||||
|
|
||||||
agent.Id = runner.Id;
|
agent.Id = runner.Id;
|
||||||
agent.Authorization = new TaskAgentAuthorization()
|
agent.Authorization = new TaskAgentAuthorization()
|
||||||
@@ -350,13 +341,6 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
AuthorizationUrl = runner.RunnerAuthorization.AuthorizationUrl,
|
AuthorizationUrl = runner.RunnerAuthorization.AuthorizationUrl,
|
||||||
ClientId = new Guid(runner.RunnerAuthorization.ClientId)
|
ClientId = new Guid(runner.RunnerAuthorization.ClientId)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(runner.RunnerAuthorization.LegacyAuthorizationUrl?.AbsoluteUri))
|
|
||||||
{
|
|
||||||
agent.Authorization.AuthorizationUrl = runner.RunnerAuthorization.LegacyAuthorizationUrl;
|
|
||||||
agent.Properties["EnableAuthMigrationByDefault"] = true;
|
|
||||||
agent.Properties["AuthorizationUrlV2"] = runner.RunnerAuthorization.AuthorizationUrl.AbsoluteUri;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -416,26 +400,13 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
||||||
throw new NotSupportedException("Message queue listen OAuth token.");
|
throw new NotSupportedException("Message queue listen OAuth token.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow the server to override the serverUrlV2 and useV2Flow
|
|
||||||
if (agent.Properties.TryGetValue("ServerUrlV2", out string serverUrlV2) &&
|
|
||||||
!string.IsNullOrEmpty(serverUrlV2))
|
|
||||||
{
|
|
||||||
Trace.Info($"Service enforced serverUrlV2: {serverUrlV2}");
|
|
||||||
runnerSettings.ServerUrlV2 = serverUrlV2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agent.Properties.TryGetValue("UseV2Flow", out bool useV2Flow) && useV2Flow)
|
|
||||||
{
|
|
||||||
Trace.Info($"Service enforced useV2Flow: {useV2Flow}");
|
|
||||||
runnerSettings.UseV2Flow = useV2Flow;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired.
|
// Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired.
|
||||||
|
|
||||||
if (!runnerSettings.UseV2Flow && !runnerSettings.UseRunnerAdminFlow)
|
if (!runnerSettings.UseV2Flow)
|
||||||
{
|
{
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||||
VssCredentials credential = credMgr.LoadCredentials(allowAuthUrlV2: false);
|
VssCredentials credential = credMgr.LoadCredentials(allowAuthUrlV2: false);
|
||||||
@@ -458,6 +429,20 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow the server to override the serverUrlV2 and useV2Flow
|
||||||
|
if (agent.Properties.TryGetValue("ServerUrlV2", out string serverUrlV2) &&
|
||||||
|
!string.IsNullOrEmpty(serverUrlV2))
|
||||||
|
{
|
||||||
|
Trace.Info($"Service enforced serverUrlV2: {serverUrlV2}");
|
||||||
|
runnerSettings.ServerUrlV2 = serverUrlV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent.Properties.TryGetValue("UseV2Flow", out bool useV2Flow) && useV2Flow)
|
||||||
|
{
|
||||||
|
Trace.Info($"Service enforced useV2Flow: {useV2Flow}");
|
||||||
|
runnerSettings.UseV2Flow = useV2Flow;
|
||||||
|
}
|
||||||
|
|
||||||
_term.WriteSection("Runner settings");
|
_term.WriteSection("Runner settings");
|
||||||
|
|
||||||
// We will Combine() what's stored with root. Defaults to string a relative path
|
// We will Combine() what's stored with root. Defaults to string a relative path
|
||||||
@@ -552,50 +537,41 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
if (isConfigured && hasCredentials)
|
if (isConfigured && hasCredentials)
|
||||||
{
|
{
|
||||||
RunnerSettings settings = _store.GetSettings();
|
RunnerSettings settings = _store.GetSettings();
|
||||||
|
var credentialManager = HostContext.GetService<ICredentialManager>();
|
||||||
|
|
||||||
if (settings.UseRunnerAdminFlow)
|
// Get the credentials
|
||||||
|
VssCredentials creds = null;
|
||||||
|
if (string.IsNullOrEmpty(settings.GitHubUrl))
|
||||||
{
|
{
|
||||||
var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove");
|
var credProvider = GetCredentialProvider(command, settings.ServerUrl);
|
||||||
await _dotcomServer.DeleteRunnerAsync(settings.GitHubUrl, deletionToken, settings.AgentId);
|
creds = credProvider.GetVssCredentials(HostContext, allowAuthUrlV2: false);
|
||||||
|
Trace.Info("legacy vss cred retrieved");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var credentialManager = HostContext.GetService<ICredentialManager>();
|
var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove");
|
||||||
|
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, deletionToken, Constants.RunnerEvent.Remove);
|
||||||
// Get the credentials
|
creds = authResult.ToVssCredentials();
|
||||||
VssCredentials creds = null;
|
Trace.Info("cred retrieved via GitHub auth");
|
||||||
if (string.IsNullOrEmpty(settings.GitHubUrl))
|
|
||||||
{
|
|
||||||
var credProvider = GetCredentialProvider(command, settings.ServerUrl);
|
|
||||||
creds = credProvider.GetVssCredentials(HostContext, allowAuthUrlV2: false);
|
|
||||||
Trace.Info("legacy vss cred retrieved");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove");
|
|
||||||
GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, deletionToken, Constants.RunnerEvent.Remove);
|
|
||||||
creds = authResult.ToVssCredentials();
|
|
||||||
Trace.Info("cred retrieved via GitHub auth");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
|
|
||||||
await _runnerServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
|
|
||||||
|
|
||||||
var agents = await _runnerServer.GetAgentsAsync(settings.AgentName);
|
|
||||||
Trace.Verbose("Returns {0} agents", agents.Count);
|
|
||||||
TaskAgent agent = agents.FirstOrDefault();
|
|
||||||
if (agent == null)
|
|
||||||
{
|
|
||||||
_term.WriteLine("Does not exist. Skipping " + currentAction);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _runnerServer.DeleteAgentAsync(settings.AgentId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_term.WriteLine();
|
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
|
||||||
_term.WriteSuccessMessage("Runner removed successfully");
|
await _runnerServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
|
||||||
|
|
||||||
|
var agents = await _runnerServer.GetAgentsAsync(settings.AgentName);
|
||||||
|
Trace.Verbose("Returns {0} agents", agents.Count);
|
||||||
|
TaskAgent agent = agents.FirstOrDefault();
|
||||||
|
if (agent == null)
|
||||||
|
{
|
||||||
|
_term.WriteLine("Does not exist. Skipping " + currentAction);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _runnerServer.DeleteAgentAsync(settings.AgentId);
|
||||||
|
|
||||||
|
_term.WriteLine();
|
||||||
|
_term.WriteSuccessMessage("Runner removed successfully");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
|
||||||
[DataMember(Name = "use_v2_flow")]
|
[DataMember(Name = "use_v2_flow")]
|
||||||
public bool UseRunnerAdminFlow { get; set; }
|
public bool UseV2Flow { get; set; }
|
||||||
|
|
||||||
public VssCredentials ToVssCredentials()
|
public VssCredentials ToVssCredentials()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1211,7 +1211,7 @@ namespace GitHub.Runner.Listener
|
|||||||
jobAnnotations.Add(annotation.Value);
|
jobAnnotations.Add(annotation.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, environmentUrl: null, telemetry: null, billingOwnerId: message.BillingOwnerId, infrastructureFailureCategory: null, CancellationToken.None);
|
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, environmentUrl: null, telemetry: null, billingOwnerId: message.BillingOwnerId, CancellationToken.None);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ namespace GitHub.Runner.Listener
|
|||||||
Task DeleteSessionAsync();
|
Task DeleteSessionAsync();
|
||||||
Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token);
|
Task<TaskAgentMessage> GetNextMessageAsync(CancellationToken token);
|
||||||
Task DeleteMessageAsync(TaskAgentMessage message);
|
Task DeleteMessageAsync(TaskAgentMessage message);
|
||||||
Task AcknowledgeMessageAsync(string runnerRequestId, CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
Task RefreshListenerTokenAsync();
|
Task RefreshListenerTokenAsync();
|
||||||
void OnJobStatus(object sender, JobStatusEventArgs e);
|
void OnJobStatus(object sender, JobStatusEventArgs e);
|
||||||
@@ -53,7 +52,7 @@ namespace GitHub.Runner.Listener
|
|||||||
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
|
private readonly TimeSpan _sessionConflictRetryLimit = TimeSpan.FromMinutes(4);
|
||||||
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
|
private readonly TimeSpan _clockSkewRetryLimit = TimeSpan.FromMinutes(30);
|
||||||
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
|
private readonly Dictionary<string, int> _sessionCreationExceptionTracker = new();
|
||||||
private TaskAgentStatus _runnerStatus = TaskAgentStatus.Online;
|
private TaskAgentStatus runnerStatus = TaskAgentStatus.Online;
|
||||||
private CancellationTokenSource _getMessagesTokenSource;
|
private CancellationTokenSource _getMessagesTokenSource;
|
||||||
private VssCredentials _creds;
|
private VssCredentials _creds;
|
||||||
private VssCredentials _credsV2;
|
private VssCredentials _credsV2;
|
||||||
@@ -218,7 +217,7 @@ namespace GitHub.Runner.Listener
|
|||||||
public void OnJobStatus(object sender, JobStatusEventArgs e)
|
public void OnJobStatus(object sender, JobStatusEventArgs e)
|
||||||
{
|
{
|
||||||
Trace.Info("Received job status event. JobState: {0}", e.Status);
|
Trace.Info("Received job status event. JobState: {0}", e.Status);
|
||||||
_runnerStatus = e.Status;
|
runnerStatus = e.Status;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_getMessagesTokenSource?.Cancel();
|
_getMessagesTokenSource?.Cancel();
|
||||||
@@ -251,7 +250,7 @@ namespace GitHub.Runner.Listener
|
|||||||
message = await _runnerServer.GetAgentMessageAsync(_settings.PoolId,
|
message = await _runnerServer.GetAgentMessageAsync(_settings.PoolId,
|
||||||
_session.SessionId,
|
_session.SessionId,
|
||||||
_lastMessageId,
|
_lastMessageId,
|
||||||
_runnerStatus,
|
runnerStatus,
|
||||||
BuildConstants.RunnerPackage.Version,
|
BuildConstants.RunnerPackage.Version,
|
||||||
VarUtil.OS,
|
VarUtil.OS,
|
||||||
VarUtil.OSArchitecture,
|
VarUtil.OSArchitecture,
|
||||||
@@ -275,7 +274,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
|
message = await _brokerServer.GetRunnerMessageAsync(_session.SessionId,
|
||||||
_runnerStatus,
|
runnerStatus,
|
||||||
BuildConstants.RunnerPackage.Version,
|
BuildConstants.RunnerPackage.Version,
|
||||||
VarUtil.OS,
|
VarUtil.OS,
|
||||||
VarUtil.OSArchitecture,
|
VarUtil.OSArchitecture,
|
||||||
@@ -438,21 +437,6 @@ namespace GitHub.Runner.Listener
|
|||||||
await _brokerServer.ForceRefreshConnection(_credsV2);
|
await _brokerServer.ForceRefreshConnection(_credsV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AcknowledgeMessageAsync(string runnerRequestId, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Short timeout
|
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
|
||||||
Trace.Info($"Acknowledging runner request '{runnerRequestId}'.");
|
|
||||||
await _brokerServer.AcknowledgeRunnerRequestAsync(
|
|
||||||
runnerRequestId,
|
|
||||||
_session.SessionId,
|
|
||||||
_runnerStatus,
|
|
||||||
BuildConstants.RunnerPackage.Version,
|
|
||||||
VarUtil.OS,
|
|
||||||
VarUtil.OSArchitecture,
|
|
||||||
linkedCts.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TaskAgentMessage DecryptMessage(TaskAgentMessage message)
|
private TaskAgentMessage DecryptMessage(TaskAgentMessage message)
|
||||||
{
|
{
|
||||||
if (_session.EncryptionKey == null ||
|
if (_session.EncryptionKey == null ||
|
||||||
|
|||||||
@@ -654,42 +654,22 @@ namespace GitHub.Runner.Listener
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var messageRef = StringUtil.ConvertFromJson<RunnerJobRequestRef>(message.Body);
|
var messageRef = StringUtil.ConvertFromJson<RunnerJobRequestRef>(message.Body);
|
||||||
|
|
||||||
// Acknowledge (best-effort)
|
|
||||||
if (messageRef.ShouldAcknowledge) // Temporary feature flag
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _listener.AcknowledgeMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Trace.Error($"Best-effort acknowledge failed for request '{messageRef.RunnerRequestId}'");
|
|
||||||
Trace.Error(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Pipelines.AgentJobRequestMessage jobRequestMessage = null;
|
Pipelines.AgentJobRequestMessage jobRequestMessage = null;
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
var credMgr = HostContext.GetService<ICredentialManager>();
|
||||||
if (string.IsNullOrEmpty(messageRef.RunServiceUrl))
|
if (string.IsNullOrEmpty(messageRef.RunServiceUrl))
|
||||||
{
|
{
|
||||||
// Connect
|
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
|
||||||
var creds = credMgr.LoadCredentials(allowAuthUrlV2: false);
|
var creds = credMgr.LoadCredentials(allowAuthUrlV2: false);
|
||||||
var actionsRunServer = HostContext.CreateService<IActionsRunServer>();
|
var actionsRunServer = HostContext.CreateService<IActionsRunServer>();
|
||||||
await actionsRunServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
|
await actionsRunServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
|
||||||
|
|
||||||
// Get job message
|
|
||||||
jobRequestMessage = await actionsRunServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
|
jobRequestMessage = await actionsRunServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Connect
|
|
||||||
var credMgr = HostContext.GetService<ICredentialManager>();
|
|
||||||
var credsV2 = credMgr.LoadCredentials(allowAuthUrlV2: true);
|
var credsV2 = credMgr.LoadCredentials(allowAuthUrlV2: true);
|
||||||
var runServer = HostContext.CreateService<IRunServer>();
|
var runServer = HostContext.CreateService<IRunServer>();
|
||||||
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), credsV2);
|
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), credsV2);
|
||||||
|
|
||||||
// Get job message
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageRef.BillingOwnerId, messageQueueLoopTokenSource.Token);
|
jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageRef.BillingOwnerId, messageQueueLoopTokenSource.Token);
|
||||||
@@ -718,10 +698,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch
|
|
||||||
jobDispatcher.Run(jobRequestMessage, runOnce);
|
jobDispatcher.Run(jobRequestMessage, runOnce);
|
||||||
|
|
||||||
// Run once?
|
|
||||||
if (runOnce)
|
if (runOnce)
|
||||||
{
|
{
|
||||||
Trace.Info("One time used runner received job message.");
|
Trace.Info("One time used runner received job message.");
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
[DataMember(Name = "runner_request_id")]
|
[DataMember(Name = "runner_request_id")]
|
||||||
public string RunnerRequestId { get; set; }
|
public string RunnerRequestId { get; set; }
|
||||||
|
|
||||||
[DataMember(Name = "should_acknowledge")]
|
|
||||||
public bool ShouldAcknowledge { get; set; }
|
|
||||||
|
|
||||||
[DataMember(Name = "run_service_url")]
|
[DataMember(Name = "run_service_url")]
|
||||||
public string RunServiceUrl { get; set; }
|
public string RunServiceUrl { get; set; }
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
// Log the error and fail the PrepareActionsAsync Initialization.
|
// Log the error and fail the PrepareActionsAsync Initialization.
|
||||||
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
||||||
executionContext.InfrastructureError(ex.Message, category: "resolve_action");
|
executionContext.InfrastructureError(ex.Message);
|
||||||
executionContext.Result = TaskResult.Failed;
|
executionContext.Result = TaskResult.Failed;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
// Log the error and fail the PrepareActionsAsync Initialization.
|
// Log the error and fail the PrepareActionsAsync Initialization.
|
||||||
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
|
||||||
executionContext.InfrastructureError(ex.Message, category: "invalid_action_download");
|
executionContext.InfrastructureError(ex.Message);
|
||||||
executionContext.Result = TaskResult.Failed;
|
executionContext.Result = TaskResult.Failed;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -777,15 +777,15 @@ namespace GitHub.Runner.Worker
|
|||||||
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
|
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
|
||||||
Directory.CreateDirectory(destDirectory);
|
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}'");
|
||||||
executionContext.Output($"Version: {downloadInfo.PackageDetails.Version}");
|
executionContext.Output($"Version: {downloadInfo.PackageDetails.Version}");
|
||||||
executionContext.Output($"Digest: {downloadInfo.PackageDetails.ManifestDigest}");
|
executionContext.Output($"Digest: {downloadInfo.PackageDetails.ManifestDigest}");
|
||||||
executionContext.Output($"Source commit SHA: {downloadInfo.ResolvedSha}");
|
executionContext.Output($"Source commit SHA: {downloadInfo.ResolvedSha}");
|
||||||
executionContext.Output("##[endgroup]");
|
executionContext.Output("##[endgroup]");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
executionContext.Output($"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}' (SHA:{downloadInfo.ResolvedSha})");
|
executionContext.Output($"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}' (SHA:{downloadInfo.ResolvedSha})");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -522,10 +522,6 @@ namespace GitHub.Runner.Worker
|
|||||||
if (annotation != null)
|
if (annotation != null)
|
||||||
{
|
{
|
||||||
stepResult.Annotations.Add(annotation.Value);
|
stepResult.Annotations.Add(annotation.Value);
|
||||||
if (annotation.Value.IsInfrastructureIssue && string.IsNullOrEmpty(Global.InfrastructureFailureCategory))
|
|
||||||
{
|
|
||||||
Global.InfrastructureFailureCategory = issue.Category;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1306,14 +1302,10 @@ namespace GitHub.Runner.Worker
|
|||||||
UpdateGlobalStepsContext();
|
UpdateGlobalStepsContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal IPipelineTemplateEvaluator ToPipelineTemplateEvaluatorInternal(ObjectTemplating.ITraceWriter traceWriter = null)
|
|
||||||
{
|
|
||||||
return new PipelineTemplateEvaluatorWrapper(HostContext, this, traceWriter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void NoOp()
|
private static void NoOp()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Error/Warning/etc methods are created as extension methods to simplify unit testing.
|
// The Error/Warning/etc methods are created as extension methods to simplify unit testing.
|
||||||
@@ -1343,9 +1335,9 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do not add a format string overload. See comment on ExecutionContext.Write().
|
// Do not add a format string overload. See comment on ExecutionContext.Write().
|
||||||
public static void InfrastructureError(this IExecutionContext context, string message, string category = null)
|
public static void InfrastructureError(this IExecutionContext context, string message)
|
||||||
{
|
{
|
||||||
var issue = new Issue() { Type = IssueType.Error, Message = message, IsInfrastructureIssue = true, Category = category };
|
var issue = new Issue() { Type = IssueType.Error, Message = message, IsInfrastructureIssue = true };
|
||||||
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
context.AddIssue(issue, ExecutionContextLogOptions.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1394,15 +1386,8 @@ namespace GitHub.Runner.Worker
|
|||||||
return new[] { new KeyValuePair<string, object>(nameof(IExecutionContext), context) };
|
return new[] { new KeyValuePair<string, object>(nameof(IExecutionContext), context) };
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IPipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null)
|
public static PipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null)
|
||||||
{
|
{
|
||||||
// Create wrapper?
|
|
||||||
if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareTemplateEvaluator) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_TEMPLATE_EVALUATOR")))
|
|
||||||
{
|
|
||||||
return (context as ExecutionContext).ToPipelineTemplateEvaluatorInternal(traceWriter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy
|
|
||||||
if (traceWriter == null)
|
if (traceWriter == null)
|
||||||
{
|
{
|
||||||
traceWriter = context.ToTemplateTraceWriter();
|
traceWriter = context.ToTemplateTraceWriter();
|
||||||
|
|||||||
@@ -22,13 +22,4 @@ namespace GitHub.Runner.Worker.Expressions
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class NewAlwaysFunction : GitHub.Actions.Expressions.Sdk.Function
|
|
||||||
{
|
|
||||||
protected override Object EvaluateCore(GitHub.Actions.Expressions.Sdk.EvaluationContext context, out GitHub.Actions.Expressions.Sdk.ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,18 +28,4 @@ namespace GitHub.Runner.Worker.Expressions
|
|||||||
return jobStatus == ActionResult.Cancelled;
|
return jobStatus == ActionResult.Cancelled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class NewCancelledFunction : GitHub.Actions.Expressions.Sdk.Function
|
|
||||||
{
|
|
||||||
protected sealed override object EvaluateCore(GitHub.Actions.Expressions.Sdk.EvaluationContext evaluationContext, out GitHub.Actions.Expressions.Sdk.ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var templateContext = evaluationContext.State as GitHub.Actions.WorkflowParser.ObjectTemplating.TemplateContext;
|
|
||||||
ArgUtil.NotNull(templateContext, nameof(templateContext));
|
|
||||||
var executionContext = templateContext.State[nameof(IExecutionContext)] as IExecutionContext;
|
|
||||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
||||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
|
||||||
return jobStatus == ActionResult.Cancelled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,29 +39,4 @@ namespace GitHub.Runner.Worker.Expressions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class NewFailureFunction : GitHub.Actions.Expressions.Sdk.Function
|
|
||||||
{
|
|
||||||
protected sealed override object EvaluateCore(GitHub.Actions.Expressions.Sdk.EvaluationContext evaluationContext, out GitHub.Actions.Expressions.Sdk.ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var templateContext = evaluationContext.State as GitHub.Actions.WorkflowParser.ObjectTemplating.TemplateContext;
|
|
||||||
ArgUtil.NotNull(templateContext, nameof(templateContext));
|
|
||||||
var executionContext = templateContext.State[nameof(IExecutionContext)] as IExecutionContext;
|
|
||||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
||||||
|
|
||||||
// Decide based on 'action_status' for composite MAIN steps and 'job.status' for pre, post and job-level steps
|
|
||||||
var isCompositeMainStep = executionContext.IsEmbedded && executionContext.Stage == ActionRunStage.Main;
|
|
||||||
if (isCompositeMainStep)
|
|
||||||
{
|
|
||||||
ActionResult actionStatus = EnumUtil.TryParse<ActionResult>(executionContext.GetGitHubContext("action_status")) ?? ActionResult.Success;
|
|
||||||
return actionStatus == ActionResult.Failure;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
|
||||||
return jobStatus == ActionResult.Failure;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,137 +143,4 @@ namespace GitHub.Runner.Worker.Expressions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class NewHashFilesFunction : GitHub.Actions.Expressions.Sdk.Function
|
|
||||||
{
|
|
||||||
private const int _hashFileTimeoutSeconds = 120;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(
|
|
||||||
GitHub.Actions.Expressions.Sdk.EvaluationContext context,
|
|
||||||
out GitHub.Actions.Expressions.Sdk.ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var templateContext = context.State as GitHub.Actions.WorkflowParser.ObjectTemplating.TemplateContext;
|
|
||||||
ArgUtil.NotNull(templateContext, nameof(templateContext));
|
|
||||||
templateContext.ExpressionValues.TryGetValue(PipelineTemplateConstants.GitHub, out var githubContextData);
|
|
||||||
ArgUtil.NotNull(githubContextData, nameof(githubContextData));
|
|
||||||
var githubContext = githubContextData as GitHub.Actions.Expressions.Data.DictionaryExpressionData;
|
|
||||||
ArgUtil.NotNull(githubContext, nameof(githubContext));
|
|
||||||
|
|
||||||
if (!githubContext.TryGetValue(PipelineTemplateConstants.HostWorkspace, out var workspace))
|
|
||||||
{
|
|
||||||
githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out workspace);
|
|
||||||
}
|
|
||||||
ArgUtil.NotNull(workspace, nameof(workspace));
|
|
||||||
|
|
||||||
var workspaceData = workspace as GitHub.Actions.Expressions.Data.StringExpressionData;
|
|
||||||
ArgUtil.NotNull(workspaceData, nameof(workspaceData));
|
|
||||||
|
|
||||||
string githubWorkspace = workspaceData.Value;
|
|
||||||
|
|
||||||
bool followSymlink = false;
|
|
||||||
List<string> patterns = new();
|
|
||||||
var firstParameter = true;
|
|
||||||
foreach (var parameter in Parameters)
|
|
||||||
{
|
|
||||||
var parameterString = parameter.Evaluate(context).ConvertToString();
|
|
||||||
if (firstParameter)
|
|
||||||
{
|
|
||||||
firstParameter = false;
|
|
||||||
if (parameterString.StartsWith("--"))
|
|
||||||
{
|
|
||||||
if (string.Equals(parameterString, "--follow-symbolic-links", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
followSymlink = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException($"Invalid glob option {parameterString}, avaliable option: '--follow-symbolic-links'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patterns.Add(parameterString);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Trace.Info($"Search root directory: '{githubWorkspace}'");
|
|
||||||
context.Trace.Info($"Search pattern: '{string.Join(", ", patterns)}'");
|
|
||||||
|
|
||||||
string binDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
|
|
||||||
string runnerRoot = new DirectoryInfo(binDir).Parent.FullName;
|
|
||||||
|
|
||||||
string node = Path.Combine(runnerRoot, "externals", NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}");
|
|
||||||
string hashFilesScript = Path.Combine(binDir, "hashFiles");
|
|
||||||
var hashResult = string.Empty;
|
|
||||||
var p = new ProcessInvoker(new NewHashFilesTrace(context.Trace));
|
|
||||||
p.ErrorDataReceived += ((_, data) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(data.Data) && data.Data.StartsWith("__OUTPUT__") && data.Data.EndsWith("__OUTPUT__"))
|
|
||||||
{
|
|
||||||
hashResult = data.Data.Substring(10, data.Data.Length - 20);
|
|
||||||
context.Trace.Info($"Hash result: '{hashResult}'");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Trace.Info(data.Data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
p.OutputDataReceived += ((_, data) =>
|
|
||||||
{
|
|
||||||
context.Trace.Info(data.Data);
|
|
||||||
});
|
|
||||||
|
|
||||||
var env = new Dictionary<string, string>();
|
|
||||||
if (followSymlink)
|
|
||||||
{
|
|
||||||
env["followSymbolicLinks"] = "true";
|
|
||||||
}
|
|
||||||
env["patterns"] = string.Join(Environment.NewLine, patterns);
|
|
||||||
|
|
||||||
using (var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(_hashFileTimeoutSeconds)))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int exitCode = p.ExecuteAsync(workingDirectory: githubWorkspace,
|
|
||||||
fileName: node,
|
|
||||||
arguments: $"\"{hashFilesScript.Replace("\"", "\\\"")}\"",
|
|
||||||
environment: env,
|
|
||||||
requireExitCodeZero: false,
|
|
||||||
cancellationToken: tokenSource.Token).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
if (exitCode != 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"hashFiles('{ExpressionUtility.StringEscape(string.Join(", ", patterns))}') failed. Fail to hash files under directory '{githubWorkspace}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (tokenSource.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
throw new TimeoutException($"hashFiles('{ExpressionUtility.StringEscape(string.Join(", ", patterns))}') couldn't finish within {_hashFileTimeoutSeconds} seconds.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return hashResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class NewHashFilesTrace : ITraceWriter
|
|
||||||
{
|
|
||||||
private GitHub.Actions.Expressions.ITraceWriter _trace;
|
|
||||||
|
|
||||||
public NewHashFilesTrace(GitHub.Actions.Expressions.ITraceWriter trace)
|
|
||||||
{
|
|
||||||
_trace = trace;
|
|
||||||
}
|
|
||||||
public void Info(string message)
|
|
||||||
{
|
|
||||||
_trace.Info(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Verbose(string message)
|
|
||||||
{
|
|
||||||
_trace.Info(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,29 +39,4 @@ namespace GitHub.Runner.Worker.Expressions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class NewSuccessFunction : GitHub.Actions.Expressions.Sdk.Function
|
|
||||||
{
|
|
||||||
protected sealed override object EvaluateCore(GitHub.Actions.Expressions.Sdk.EvaluationContext evaluationContext, out GitHub.Actions.Expressions.Sdk.ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var templateContext = evaluationContext.State as GitHub.Actions.WorkflowParser.ObjectTemplating.TemplateContext;
|
|
||||||
ArgUtil.NotNull(templateContext, nameof(templateContext));
|
|
||||||
var executionContext = templateContext.State[nameof(IExecutionContext)] as IExecutionContext;
|
|
||||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
||||||
|
|
||||||
// Decide based on 'action_status' for composite MAIN steps and 'job.status' for pre, post and job-level steps
|
|
||||||
var isCompositeMainStep = executionContext.IsEmbedded && executionContext.Stage == ActionRunStage.Main;
|
|
||||||
if (isCompositeMainStep)
|
|
||||||
{
|
|
||||||
ActionResult actionStatus = EnumUtil.TryParse<ActionResult>(executionContext.GetGitHubContext("action_status")) ?? ActionResult.Success;
|
|
||||||
return actionStatus == ActionResult.Success;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ActionResult jobStatus = executionContext.JobContext.Status ?? ActionResult.Success;
|
|
||||||
return jobStatus == ActionResult.Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,5 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ 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 string InfrastructureFailureCategory { get; set; }
|
|
||||||
public JObject ContainerHookState { get; set; }
|
public JObject ContainerHookState { get; set; }
|
||||||
public bool HasTemplateEvaluatorMismatch { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,19 +191,11 @@ 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"));
|
||||||
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"));
|
||||||
|
|
||||||
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");
|
||||||
|
|||||||
@@ -58,41 +58,10 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
var nodeData = data as NodeJSActionExecutionData;
|
var nodeData = data as NodeJSActionExecutionData;
|
||||||
|
|
||||||
// With node12 EoL in 04/2022 and node16 EoL in 09/23, we want to execute all JS actions using node20
|
// With node12 EoL in 04/2022 and node16 EoL in 09/23, we want to execute all JS actions using node20
|
||||||
// With node20 EoL approaching, we're preparing to migrate to node24
|
|
||||||
if (string.Equals(nodeData.NodeVersion, "node12", StringComparison.InvariantCultureIgnoreCase) ||
|
if (string.Equals(nodeData.NodeVersion, "node12", StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
string.Equals(nodeData.NodeVersion, "node16", StringComparison.InvariantCultureIgnoreCase))
|
string.Equals(nodeData.NodeVersion, "node16", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20;
|
nodeData.NodeVersion = "node20";
|
||||||
}
|
|
||||||
|
|
||||||
// Check if node20 was explicitly specified in the action
|
|
||||||
// We don't modify if node24 was explicitly specified
|
|
||||||
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
bool useNode24ByDefault = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.UseNode24ByDefaultFlag) ?? false;
|
|
||||||
bool requireNode24 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.RequireNode24Flag) ?? false;
|
|
||||||
|
|
||||||
var (nodeVersion, configWarningMessage) = NodeUtil.DetermineActionsNodeVersion(environment, useNode24ByDefault, requireNode24);
|
|
||||||
var (finalNodeVersion, platformWarningMessage) = NodeUtil.CheckNodeVersionForLinuxArm32(nodeVersion);
|
|
||||||
nodeData.NodeVersion = finalNodeVersion;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(configWarningMessage))
|
|
||||||
{
|
|
||||||
executionContext.Warning(configWarningMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(platformWarningMessage))
|
|
||||||
{
|
|
||||||
executionContext.Warning(platformWarningMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show information about Node 24 migration in Phase 2
|
|
||||||
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. " +
|
|
||||||
"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable.";
|
|
||||||
executionContext.Output(infoMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(handler as INodeScriptActionHandler).Data = nodeData;
|
(handler as INodeScriptActionHandler).Data = nodeData;
|
||||||
|
|||||||
@@ -400,10 +400,6 @@ namespace GitHub.Runner.Worker
|
|||||||
if (snapshotRequest != null)
|
if (snapshotRequest != null)
|
||||||
{
|
{
|
||||||
var snapshotOperationProvider = HostContext.GetService<ISnapshotOperationProvider>();
|
var snapshotOperationProvider = HostContext.GetService<ISnapshotOperationProvider>();
|
||||||
// Check that that runner is capable of taking a snapshot
|
|
||||||
snapshotOperationProvider.RunSnapshotPreflightChecks(context);
|
|
||||||
|
|
||||||
// Add postjob step to write snapshot file
|
|
||||||
jobContext.RegisterPostJobStep(new JobExtensionRunner(
|
jobContext.RegisterPostJobStep(new JobExtensionRunner(
|
||||||
runAsync: (executionContext, _) => snapshotOperationProvider.CreateSnapshotRequestAsync(executionContext, snapshotRequest),
|
runAsync: (executionContext, _) => snapshotOperationProvider.CreateSnapshotRequestAsync(executionContext, snapshotRequest),
|
||||||
condition: snapshotRequest.Condition,
|
condition: snapshotRequest.Condition,
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, infrastructureFailureCategory: jobContext.Global.InfrastructureFailureCategory, default);
|
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, telemetry, billingOwnerId: message.BillingOwnerId, default);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (VssUnauthorizedException ex)
|
catch (VssUnauthorizedException ex)
|
||||||
|
|||||||
@@ -1,679 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GitHub.Actions.WorkflowParser;
|
|
||||||
using GitHub.DistributedTask.Expressions2;
|
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
|
||||||
using GitHub.DistributedTask.Pipelines;
|
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
||||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common;
|
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
|
||||||
{
|
|
||||||
internal sealed class PipelineTemplateEvaluatorWrapper : IPipelineTemplateEvaluator
|
|
||||||
{
|
|
||||||
private PipelineTemplateEvaluator _legacyEvaluator;
|
|
||||||
private WorkflowTemplateEvaluator _newEvaluator;
|
|
||||||
private IExecutionContext _context;
|
|
||||||
private Tracing _trace;
|
|
||||||
|
|
||||||
public PipelineTemplateEvaluatorWrapper(
|
|
||||||
IHostContext hostContext,
|
|
||||||
IExecutionContext context,
|
|
||||||
ObjectTemplating.ITraceWriter traceWriter = null)
|
|
||||||
{
|
|
||||||
ArgUtil.NotNull(hostContext, nameof(hostContext));
|
|
||||||
ArgUtil.NotNull(context, nameof(context));
|
|
||||||
_context = context;
|
|
||||||
_trace = hostContext.GetTrace(nameof(PipelineTemplateEvaluatorWrapper));
|
|
||||||
|
|
||||||
if (traceWriter == null)
|
|
||||||
{
|
|
||||||
traceWriter = context.ToTemplateTraceWriter();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy evaluator
|
|
||||||
var schema = PipelineTemplateSchemaFactory.GetSchema();
|
|
||||||
_legacyEvaluator = new PipelineTemplateEvaluator(traceWriter, schema, context.Global.FileTable)
|
|
||||||
{
|
|
||||||
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
|
|
||||||
};
|
|
||||||
|
|
||||||
// New evaluator
|
|
||||||
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
|
|
||||||
_newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, context.Global.FileTable, features: null)
|
|
||||||
{
|
|
||||||
MaxErrorMessageLength = int.MaxValue, // Don't truncate error messages otherwise we might not scrub secrets correctly
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool EvaluateStepContinueOnError(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateStepContinueOnError",
|
|
||||||
() => _legacyEvaluator.EvaluateStepContinueOnError(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateStepContinueOnError(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
(legacyResult, newResult) => legacyResult == newResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string EvaluateStepDisplayName(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateStepDisplayName",
|
|
||||||
() => _legacyEvaluator.EvaluateStepDisplayName(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateStepName(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
(legacyResult, newResult) => string.Equals(legacyResult, newResult, StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<string, string> EvaluateStepEnvironment(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions,
|
|
||||||
StringComparer keyComparer)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateStepEnvironment",
|
|
||||||
() => _legacyEvaluator.EvaluateStepEnvironment(token, contextData, expressionFunctions, keyComparer),
|
|
||||||
() => _newEvaluator.EvaluateStepEnvironment(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions), keyComparer),
|
|
||||||
CompareStepEnvironment);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool EvaluateStepIf(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions,
|
|
||||||
IEnumerable<KeyValuePair<string, object>> expressionState)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateStepIf",
|
|
||||||
() => _legacyEvaluator.EvaluateStepIf(token, contextData, expressionFunctions, expressionState),
|
|
||||||
() => _newEvaluator.EvaluateStepIf(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions), expressionState),
|
|
||||||
(legacyResult, newResult) => legacyResult == newResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<string, string> EvaluateStepInputs(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateStepInputs",
|
|
||||||
() => _legacyEvaluator.EvaluateStepInputs(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateStepInputs(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
(legacyResult, newResult) => CompareDictionaries(legacyResult, newResult, "StepInputs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int EvaluateStepTimeout(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateStepTimeout",
|
|
||||||
() => _legacyEvaluator.EvaluateStepTimeout(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateStepTimeout(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
(legacyResult, newResult) => legacyResult == newResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GitHub.DistributedTask.Pipelines.JobContainer EvaluateJobContainer(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateJobContainer",
|
|
||||||
() => _legacyEvaluator.EvaluateJobContainer(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateJobContainer(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
CompareJobContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<string, string> EvaluateJobOutput(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateJobOutput",
|
|
||||||
() => _legacyEvaluator.EvaluateJobOutput(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateJobOutputs(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
(legacyResult, newResult) => CompareDictionaries(legacyResult, newResult, "JobOutput"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public TemplateToken EvaluateEnvironmentUrl(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateEnvironmentUrl",
|
|
||||||
() => _legacyEvaluator.EvaluateEnvironmentUrl(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateJobEnvironmentUrl(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
CompareEnvironmentUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<string, string> EvaluateJobDefaultsRun(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateJobDefaultsRun",
|
|
||||||
() => _legacyEvaluator.EvaluateJobDefaultsRun(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateJobDefaultsRun(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
(legacyResult, newResult) => CompareDictionaries(legacyResult, newResult, "JobDefaultsRun"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<KeyValuePair<string, GitHub.DistributedTask.Pipelines.JobContainer>> EvaluateJobServiceContainers(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateJobServiceContainers",
|
|
||||||
() => _legacyEvaluator.EvaluateJobServiceContainers(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateJobServiceContainers(ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
(legacyResult, newResult) => CompareJobServiceContainers(legacyResult, newResult));
|
|
||||||
}
|
|
||||||
|
|
||||||
public GitHub.DistributedTask.Pipelines.Snapshot EvaluateJobSnapshotRequest(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
return EvaluateAndCompare(
|
|
||||||
"EvaluateJobSnapshotRequest",
|
|
||||||
() => _legacyEvaluator.EvaluateJobSnapshotRequest(token, contextData, expressionFunctions),
|
|
||||||
() => _newEvaluator.EvaluateSnapshot(string.Empty, ConvertToken(token), ConvertData(contextData), ConvertFunctions(expressionFunctions)),
|
|
||||||
CompareSnapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecordMismatch(string methodName)
|
|
||||||
{
|
|
||||||
if (!_context.Global.HasTemplateEvaluatorMismatch)
|
|
||||||
{
|
|
||||||
_context.Global.HasTemplateEvaluatorMismatch = true;
|
|
||||||
var telemetry = new JobTelemetry { Type = JobTelemetryType.General, Message = $"TemplateEvaluatorMismatch: {methodName}" };
|
|
||||||
_context.Global.JobTelemetry.Add(telemetry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecordComparisonError(string errorDetails)
|
|
||||||
{
|
|
||||||
if (!_context.Global.HasTemplateEvaluatorMismatch)
|
|
||||||
{
|
|
||||||
_context.Global.HasTemplateEvaluatorMismatch = true;
|
|
||||||
var telemetry = new JobTelemetry { Type = JobTelemetryType.General, Message = $"TemplateEvaluatorComparisonError: {errorDetails}" };
|
|
||||||
_context.Global.JobTelemetry.Add(telemetry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TLegacy EvaluateAndCompare<TLegacy, TNew>(
|
|
||||||
string methodName,
|
|
||||||
Func<TLegacy> legacyEvaluator,
|
|
||||||
Func<TNew> newEvaluator,
|
|
||||||
Func<TLegacy, TNew, bool> resultComparer)
|
|
||||||
{
|
|
||||||
// Legacy evaluator
|
|
||||||
var legacyException = default(Exception);
|
|
||||||
var legacyResult = default(TLegacy);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
legacyResult = legacyEvaluator();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
legacyException = ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare with new evaluator
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ArgUtil.NotNull(_context, nameof(_context));
|
|
||||||
ArgUtil.NotNull(_newEvaluator, nameof(_newEvaluator));
|
|
||||||
_trace.Info(methodName);
|
|
||||||
|
|
||||||
// New evaluator
|
|
||||||
var newException = default(Exception);
|
|
||||||
var newResult = default(TNew);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
newResult = newEvaluator();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
newException = ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare results or exceptions
|
|
||||||
if (legacyException != null || newException != null)
|
|
||||||
{
|
|
||||||
// Either one or both threw exceptions - compare them
|
|
||||||
if (!CompareExceptions(legacyException, newException))
|
|
||||||
{
|
|
||||||
_trace.Info($"{methodName} exception mismatch");
|
|
||||||
RecordMismatch($"{methodName}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Both succeeded - compare results
|
|
||||||
if (!resultComparer(legacyResult, newResult))
|
|
||||||
{
|
|
||||||
_trace.Info($"{methodName} mismatch");
|
|
||||||
RecordMismatch($"{methodName}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_trace.Info($"Comparison failed: {ex.Message}");
|
|
||||||
RecordComparisonError($"{methodName}: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-throw legacy exception if any
|
|
||||||
if (legacyException != null)
|
|
||||||
{
|
|
||||||
throw legacyException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return legacyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken ConvertToken(
|
|
||||||
GitHub.DistributedTask.ObjectTemplating.Tokens.TemplateToken token)
|
|
||||||
{
|
|
||||||
if (token == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = StringUtil.ConvertToJson(token, Newtonsoft.Json.Formatting.None);
|
|
||||||
return StringUtil.ConvertFromJson<GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken>(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
private GitHub.Actions.Expressions.Data.DictionaryExpressionData ConvertData(
|
|
||||||
GitHub.DistributedTask.Pipelines.ContextData.DictionaryContextData contextData)
|
|
||||||
{
|
|
||||||
if (contextData == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = StringUtil.ConvertToJson(contextData, Newtonsoft.Json.Formatting.None);
|
|
||||||
return StringUtil.ConvertFromJson<GitHub.Actions.Expressions.Data.DictionaryExpressionData>(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IList<GitHub.Actions.Expressions.IFunctionInfo> ConvertFunctions(
|
|
||||||
IList<GitHub.DistributedTask.Expressions2.IFunctionInfo> expressionFunctions)
|
|
||||||
{
|
|
||||||
if (expressionFunctions == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new List<GitHub.Actions.Expressions.IFunctionInfo>();
|
|
||||||
foreach (var func in expressionFunctions)
|
|
||||||
{
|
|
||||||
GitHub.Actions.Expressions.IFunctionInfo newFunc = func.Name switch
|
|
||||||
{
|
|
||||||
"always" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewAlwaysFunction>(func.Name, func.MinParameters, func.MaxParameters),
|
|
||||||
"cancelled" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewCancelledFunction>(func.Name, func.MinParameters, func.MaxParameters),
|
|
||||||
"failure" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewFailureFunction>(func.Name, func.MinParameters, func.MaxParameters),
|
|
||||||
"success" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewSuccessFunction>(func.Name, func.MinParameters, func.MaxParameters),
|
|
||||||
"hashFiles" => new GitHub.Actions.Expressions.FunctionInfo<Expressions.NewHashFilesFunction>(func.Name, func.MinParameters, func.MaxParameters),
|
|
||||||
_ => throw new NotSupportedException($"Expression function '{func.Name}' is not supported for conversion")
|
|
||||||
};
|
|
||||||
result.Add(newFunc);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareStepEnvironment(
|
|
||||||
Dictionary<string, string> legacyResult,
|
|
||||||
Dictionary<string, string> newResult)
|
|
||||||
{
|
|
||||||
return CompareDictionaries(legacyResult, newResult, "StepEnvironment");
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareEnvironmentUrl(
|
|
||||||
TemplateToken legacyResult,
|
|
||||||
GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken newResult)
|
|
||||||
{
|
|
||||||
var legacyJson = legacyResult != null ? Newtonsoft.Json.JsonConvert.SerializeObject(legacyResult, Newtonsoft.Json.Formatting.None) : null;
|
|
||||||
var newJson = newResult != null ? Newtonsoft.Json.JsonConvert.SerializeObject(newResult, Newtonsoft.Json.Formatting.None) : null;
|
|
||||||
return legacyJson == newJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareJobContainer(
|
|
||||||
GitHub.DistributedTask.Pipelines.JobContainer legacyResult,
|
|
||||||
GitHub.Actions.WorkflowParser.JobContainer newResult)
|
|
||||||
{
|
|
||||||
if (legacyResult == null && newResult == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyResult == null || newResult == null)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobContainer mismatch - one result is null (legacy={legacyResult == null}, new={newResult == null})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(legacyResult.Image, newResult.Image, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobContainer mismatch - Image differs (legacy='{legacyResult.Image}', new='{newResult.Image}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(legacyResult.Options, newResult.Options, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobContainer mismatch - Options differs (legacy='{legacyResult.Options}', new='{newResult.Options}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CompareDictionaries(legacyResult.Environment, newResult.Environment, "Environment"))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CompareLists(legacyResult.Volumes, newResult.Volumes, "Volumes"))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CompareLists(legacyResult.Ports, newResult.Ports, "Ports"))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CompareCredentials(legacyResult.Credentials, newResult.Credentials))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareCredentials(
|
|
||||||
GitHub.DistributedTask.Pipelines.ContainerRegistryCredentials legacyCreds,
|
|
||||||
GitHub.Actions.WorkflowParser.ContainerRegistryCredentials newCreds)
|
|
||||||
{
|
|
||||||
if (legacyCreds == null && newCreds == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyCreds == null || newCreds == null)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareCredentials mismatch - one is null (legacy={legacyCreds == null}, new={newCreds == null})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(legacyCreds.Username, newCreds.Username, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareCredentials mismatch - Credentials.Username differs (legacy='{legacyCreds.Username}', new='{newCreds.Username}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(legacyCreds.Password, newCreds.Password, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareCredentials mismatch - Credentials.Password differs");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareLists(IList<string> legacyList, IList<string> newList, string fieldName)
|
|
||||||
{
|
|
||||||
if (legacyList == null && newList == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyList == null || newList == null)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareLists mismatch - {fieldName} - one is null (legacy={legacyList == null}, new={newList == null})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyList.Count != newList.Count)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareLists mismatch - {fieldName}.Count differs (legacy={legacyList.Count}, new={newList.Count})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < legacyList.Count; i++)
|
|
||||||
{
|
|
||||||
if (!string.Equals(legacyList[i], newList[i], StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareLists mismatch - {fieldName}[{i}] differs (legacy='{legacyList[i]}', new='{newList[i]}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareDictionaries(IDictionary<string, string> legacyDict, IDictionary<string, string> newDict, string fieldName)
|
|
||||||
{
|
|
||||||
if (legacyDict == null && newDict == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyDict == null || newDict == null)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareDictionaries mismatch - {fieldName} - one is null (legacy={legacyDict == null}, new={newDict == null})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyDict is Dictionary<String, String> legacyTypedDict && newDict is Dictionary<String, String> newTypedDict)
|
|
||||||
{
|
|
||||||
if (!object.Equals(legacyTypedDict.Comparer, newTypedDict.Comparer))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareDictionaries mismatch - {fieldName} - different comparers (legacy={legacyTypedDict.Comparer.GetType().Name}, new={newTypedDict.Comparer.GetType().Name})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyDict.Count != newDict.Count)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareDictionaries mismatch - {fieldName}.Count differs (legacy={legacyDict.Count}, new={newDict.Count})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var kvp in legacyDict)
|
|
||||||
{
|
|
||||||
if (!newDict.TryGetValue(kvp.Key, out var newValue))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareDictionaries mismatch - {fieldName} - key '{kvp.Key}' missing in new result");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(kvp.Value, newValue, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareDictionaries mismatch - {fieldName}['{kvp.Key}'] differs (legacy='{kvp.Value}', new='{newValue}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareJobServiceContainers(
|
|
||||||
IList<KeyValuePair<string, GitHub.DistributedTask.Pipelines.JobContainer>> legacyResult,
|
|
||||||
IList<KeyValuePair<string, GitHub.Actions.WorkflowParser.JobContainer>> newResult)
|
|
||||||
{
|
|
||||||
if (legacyResult == null && newResult == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyResult == null || newResult == null)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobServiceContainers mismatch - one result is null (legacy={legacyResult == null}, new={newResult == null})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyResult.Count != newResult.Count)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobServiceContainers mismatch - ServiceContainers.Count differs (legacy={legacyResult.Count}, new={newResult.Count})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < legacyResult.Count; i++)
|
|
||||||
{
|
|
||||||
var legacyKvp = legacyResult[i];
|
|
||||||
var newKvp = newResult[i];
|
|
||||||
|
|
||||||
if (!string.Equals(legacyKvp.Key, newKvp.Key, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobServiceContainers mismatch - ServiceContainers[{i}].Key differs (legacy='{legacyKvp.Key}', new='{newKvp.Key}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CompareJobContainer(legacyKvp.Value, newKvp.Value))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareJobServiceContainers mismatch - ServiceContainers['{legacyKvp.Key}']");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareSnapshot(
|
|
||||||
GitHub.DistributedTask.Pipelines.Snapshot legacyResult,
|
|
||||||
GitHub.Actions.WorkflowParser.Snapshot newResult)
|
|
||||||
{
|
|
||||||
if (legacyResult == null && newResult == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyResult == null || newResult == null)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareSnapshot mismatch - one is null (legacy={legacyResult == null}, new={newResult == null})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(legacyResult.ImageName, newResult.ImageName, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareSnapshot mismatch - Snapshot.ImageName differs (legacy='{legacyResult.ImageName}', new='{newResult.ImageName}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(legacyResult.Version, newResult.Version, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareSnapshot mismatch - Snapshot.Version differs (legacy='{legacyResult.Version}', new='{newResult.Version}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare Condition (legacy) vs If (new)
|
|
||||||
// Legacy has Condition as string, new has If as BasicExpressionToken
|
|
||||||
// For comparison, we'll serialize the If token and compare with Condition
|
|
||||||
var newIfValue = newResult.If != null ? Newtonsoft.Json.JsonConvert.SerializeObject(newResult.If, Newtonsoft.Json.Formatting.None) : null;
|
|
||||||
|
|
||||||
// Legacy Condition is a string expression like "success()"
|
|
||||||
// New If is a BasicExpressionToken that needs to be serialized
|
|
||||||
// We'll do a basic comparison - if both are null/empty or both exist
|
|
||||||
var legacyHasCondition = !string.IsNullOrEmpty(legacyResult.Condition);
|
|
||||||
var newHasIf = newResult.If != null;
|
|
||||||
|
|
||||||
if (legacyHasCondition != newHasIf)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareSnapshot mismatch - condition/if presence differs (legacy has condition={legacyHasCondition}, new has if={newHasIf})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CompareExceptions(Exception legacyException, Exception newException)
|
|
||||||
{
|
|
||||||
if (legacyException == null && newException == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (legacyException == null || newException == null)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareExceptions mismatch - one exception is null (legacy={legacyException == null}, new={newException == null})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare exception messages recursively (including inner exceptions)
|
|
||||||
var legacyMessages = GetExceptionMessages(legacyException);
|
|
||||||
var newMessages = GetExceptionMessages(newException);
|
|
||||||
|
|
||||||
if (legacyMessages.Count != newMessages.Count)
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareExceptions mismatch - different number of exception messages (legacy={legacyMessages.Count}, new={newMessages.Count})");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < legacyMessages.Count; i++)
|
|
||||||
{
|
|
||||||
if (!string.Equals(legacyMessages[i], newMessages[i], StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_trace.Info($"CompareExceptions mismatch - exception messages differ at level {i} (legacy='{legacyMessages[i]}', new='{newMessages[i]}')");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IList<string> GetExceptionMessages(Exception ex)
|
|
||||||
{
|
|
||||||
var messages = new List<string>();
|
|
||||||
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;
|
|
||||||
|
|
||||||
messages.Add(current.Message);
|
|
||||||
count++;
|
|
||||||
|
|
||||||
// Special handling for AggregateException - enqueue all inner exceptions
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failsafe: if we have too many exceptions, stop and return what we have
|
|
||||||
if (count >= 50)
|
|
||||||
{
|
|
||||||
_trace.Info("CompareExceptions failsafe triggered - too many exceptions (50+)");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.Pipelines;
|
using GitHub.DistributedTask.Pipelines;
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Handlers;
|
|
||||||
namespace GitHub.Runner.Worker;
|
namespace GitHub.Runner.Worker;
|
||||||
|
|
||||||
[ServiceLocator(Default = typeof(SnapshotOperationProvider))]
|
[ServiceLocator(Default = typeof(SnapshotOperationProvider))]
|
||||||
public interface ISnapshotOperationProvider : IRunnerService
|
public interface ISnapshotOperationProvider : IRunnerService
|
||||||
{
|
{
|
||||||
Task CreateSnapshotRequestAsync(IExecutionContext executionContext, Snapshot snapshotRequest);
|
Task CreateSnapshotRequestAsync(IExecutionContext executionContext, Snapshot snapshotRequest);
|
||||||
void RunSnapshotPreflightChecks(IExecutionContext jobContext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnapshotOperationProvider : RunnerService, ISnapshotOperationProvider
|
public class SnapshotOperationProvider : RunnerService, ISnapshotOperationProvider
|
||||||
@@ -28,32 +24,9 @@ public class SnapshotOperationProvider : RunnerService, ISnapshotOperationProvid
|
|||||||
}
|
}
|
||||||
|
|
||||||
IOUtil.SaveObject(snapshotRequest, snapshotRequestFilePath);
|
IOUtil.SaveObject(snapshotRequest, snapshotRequestFilePath);
|
||||||
executionContext.Output($"Image Name: {snapshotRequest.ImageName} Version: {snapshotRequest.Version}");
|
|
||||||
executionContext.Output($"Request written to: {snapshotRequestFilePath}");
|
executionContext.Output($"Request written to: {snapshotRequestFilePath}");
|
||||||
executionContext.Output("This request will be processed after the job completes. You will not receive any feedback on the snapshot process within the workflow logs of this job.");
|
executionContext.Output("This request will be processed after the job completes. You will not receive any feedback on the snapshot process within the workflow logs of this job.");
|
||||||
executionContext.Output("If the snapshot process is successful, you should see a new image with the requested name in the list of available custom images when creating a new GitHub-hosted Runner.");
|
executionContext.Output("If the snapshot process is successful, you should see a new image with the requested name in the list of available custom images when creating a new GitHub-hosted Runner.");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RunSnapshotPreflightChecks(IExecutionContext context)
|
|
||||||
{
|
|
||||||
var shouldCheckRunnerEnvironment = context.Global.Variables.GetBoolean(Constants.Runner.Features.SnapshotPreflightHostedRunnerCheck) ?? false;
|
|
||||||
if (shouldCheckRunnerEnvironment &&
|
|
||||||
context.Global.Variables.TryGetValue(WellKnownDistributedTaskVariables.RunnerEnvironment, out var runnerEnvironment) &&
|
|
||||||
!string.IsNullOrEmpty(runnerEnvironment))
|
|
||||||
{
|
|
||||||
context.Debug($"Snapshot: RUNNER_ENVIRONMENT={runnerEnvironment}");
|
|
||||||
if (!string.Equals(runnerEnvironment, "github-hosted", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Snapshot workflows must be run on a GitHub Hosted Runner");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var imageGenEnabled = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED"));
|
|
||||||
context.Debug($"Snapshot: GITHUB_ACTIONS_IMAGE_GEN_ENABLED={imageGenEnabled}");
|
|
||||||
var shouldCheckImageGenPool = context.Global.Variables.GetBoolean(Constants.Runner.Features.SnapshotPreflightImageGenPoolCheck) ?? false;
|
|
||||||
if (shouldCheckImageGenPool && !imageGenEnabled)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Snapshot workflows must be run a hosted runner with Image Generation enabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using GitHub.DistributedTask.Expressions2;
|
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates parts of the workflow DOM. For example, a job strategy or step inputs.
|
|
||||||
/// </summary>
|
|
||||||
public interface IPipelineTemplateEvaluator
|
|
||||||
{
|
|
||||||
Boolean EvaluateStepContinueOnError(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
String EvaluateStepDisplayName(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
Dictionary<String, String> EvaluateStepEnvironment(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions,
|
|
||||||
StringComparer keyComparer);
|
|
||||||
|
|
||||||
Boolean EvaluateStepIf(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions,
|
|
||||||
IEnumerable<KeyValuePair<String, Object>> expressionState);
|
|
||||||
|
|
||||||
Dictionary<String, String> EvaluateStepInputs(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
Int32 EvaluateStepTimeout(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
JobContainer EvaluateJobContainer(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
Dictionary<String, String> EvaluateJobOutput(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
TemplateToken EvaluateEnvironmentUrl(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
Dictionary<String, String> EvaluateJobDefaultsRun(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
IList<KeyValuePair<String, JobContainer>> EvaluateJobServiceContainers(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
|
|
||||||
Snapshot EvaluateJobSnapshotRequest(
|
|
||||||
TemplateToken token,
|
|
||||||
DictionaryContextData contextData,
|
|
||||||
IList<IFunctionInfo> expressionFunctions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
|||||||
/// Evaluates parts of the workflow DOM. For example, a job strategy or step inputs.
|
/// Evaluates parts of the workflow DOM. For example, a job strategy or step inputs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public class PipelineTemplateEvaluator : IPipelineTemplateEvaluator
|
public class PipelineTemplateEvaluator
|
||||||
{
|
{
|
||||||
public PipelineTemplateEvaluator(
|
public PipelineTemplateEvaluator(
|
||||||
ITraceWriter trace,
|
ITraceWriter trace,
|
||||||
|
|||||||
@@ -18,16 +18,6 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
internal set;
|
internal set;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The url to refresh tokens with legacy service
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("legacy_authorization_url")]
|
|
||||||
public Uri LegacyAuthorizationUrl
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
internal set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The url to connect to poll for messages
|
/// The url to connect to poll for messages
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,111 +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 System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
[JsonObject]
|
|
||||||
public sealed class ArrayExpressionData : ExpressionData, IEnumerable<ExpressionData>, IReadOnlyArray
|
|
||||||
{
|
|
||||||
public ArrayExpressionData()
|
|
||||||
: base(ExpressionDataType.Array)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[IgnoreDataMember]
|
|
||||||
public Int32 Count => m_items?.Count ?? 0;
|
|
||||||
|
|
||||||
public ExpressionData this[Int32 index] => m_items[index];
|
|
||||||
|
|
||||||
Object IReadOnlyArray.this[Int32 index] => m_items[index];
|
|
||||||
|
|
||||||
public void Add(ExpressionData item)
|
|
||||||
{
|
|
||||||
if (m_items == null)
|
|
||||||
{
|
|
||||||
m_items = new List<ExpressionData>();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_items.Add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ExpressionData Clone()
|
|
||||||
{
|
|
||||||
var result = new ArrayExpressionData();
|
|
||||||
if (m_items?.Count > 0)
|
|
||||||
{
|
|
||||||
result.m_items = new List<ExpressionData>(m_items.Count);
|
|
||||||
foreach (var item in m_items)
|
|
||||||
{
|
|
||||||
result.m_items.Add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override JToken ToJToken()
|
|
||||||
{
|
|
||||||
var result = new JArray();
|
|
||||||
if (m_items?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var item in m_items)
|
|
||||||
{
|
|
||||||
result.Add(item?.ToJToken() ?? JValue.CreateNull());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<ExpressionData> GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_items?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var item in m_items)
|
|
||||||
{
|
|
||||||
yield return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_items?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var item in m_items)
|
|
||||||
{
|
|
||||||
yield return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IReadOnlyArray.GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_items?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var item in m_items)
|
|
||||||
{
|
|
||||||
yield return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[OnSerializing]
|
|
||||||
private void OnSerializing(StreamingContext context)
|
|
||||||
{
|
|
||||||
if (m_items?.Count == 0)
|
|
||||||
{
|
|
||||||
m_items = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "a", EmitDefaultValue = false)]
|
|
||||||
private List<ExpressionData> m_items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
public sealed class BooleanExpressionData : ExpressionData, IBoolean
|
|
||||||
{
|
|
||||||
public BooleanExpressionData(Boolean value)
|
|
||||||
: base(ExpressionDataType.Boolean)
|
|
||||||
{
|
|
||||||
m_value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean Value
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ExpressionData Clone()
|
|
||||||
{
|
|
||||||
return new BooleanExpressionData(m_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override JToken ToJToken()
|
|
||||||
{
|
|
||||||
return (JToken)m_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override String ToString()
|
|
||||||
{
|
|
||||||
return m_value ? "true" : "false";
|
|
||||||
}
|
|
||||||
|
|
||||||
Boolean IBoolean.GetBoolean()
|
|
||||||
{
|
|
||||||
return Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator Boolean(BooleanExpressionData data)
|
|
||||||
{
|
|
||||||
return data.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator BooleanExpressionData(Boolean data)
|
|
||||||
{
|
|
||||||
return new BooleanExpressionData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "b", EmitDefaultValue = false)]
|
|
||||||
private Boolean m_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,289 +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 System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
[JsonObject]
|
|
||||||
public class CaseSensitiveDictionaryExpressionData : ExpressionData, IEnumerable<KeyValuePair<String, ExpressionData>>, IReadOnlyObject
|
|
||||||
{
|
|
||||||
public CaseSensitiveDictionaryExpressionData()
|
|
||||||
: base(ExpressionDataType.CaseSensitiveDictionary)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[IgnoreDataMember]
|
|
||||||
public Int32 Count => m_list?.Count ?? 0;
|
|
||||||
|
|
||||||
[IgnoreDataMember]
|
|
||||||
public IEnumerable<String> Keys
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return pair.Key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[IgnoreDataMember]
|
|
||||||
public IEnumerable<ExpressionData> Values
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return pair.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<Object> IReadOnlyObject.Values
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return pair.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<String, Int32> IndexLookup
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_indexLookup == null)
|
|
||||||
{
|
|
||||||
m_indexLookup = new Dictionary<String, Int32>(StringComparer.Ordinal);
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < m_list.Count; i++)
|
|
||||||
{
|
|
||||||
var pair = m_list[i];
|
|
||||||
m_indexLookup.Add(pair.Key, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_indexLookup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<DictionaryExpressionDataPair> List
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_list == null)
|
|
||||||
{
|
|
||||||
m_list = new List<DictionaryExpressionDataPair>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExpressionData this[String key]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var index = IndexLookup[key];
|
|
||||||
return m_list[index].Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
// Existing
|
|
||||||
if (IndexLookup.TryGetValue(key, out var index))
|
|
||||||
{
|
|
||||||
key = m_list[index].Key; // preserve casing
|
|
||||||
m_list[index] = new DictionaryExpressionDataPair(key, value);
|
|
||||||
}
|
|
||||||
// New
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Add(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Object IReadOnlyObject.this[String key]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var index = IndexLookup[key];
|
|
||||||
return m_list[index].Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal KeyValuePair<String, ExpressionData> this[Int32 index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var pair = m_list[index];
|
|
||||||
return new KeyValuePair<String, ExpressionData>(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(IEnumerable<KeyValuePair<String, ExpressionData>> pairs)
|
|
||||||
{
|
|
||||||
foreach (var pair in pairs)
|
|
||||||
{
|
|
||||||
Add(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(
|
|
||||||
String key,
|
|
||||||
ExpressionData value)
|
|
||||||
{
|
|
||||||
IndexLookup.Add(key, m_list?.Count ?? 0);
|
|
||||||
List.Add(new DictionaryExpressionDataPair(key, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ExpressionData Clone()
|
|
||||||
{
|
|
||||||
var result = new CaseSensitiveDictionaryExpressionData();
|
|
||||||
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
result.m_list = new List<DictionaryExpressionDataPair>(m_list.Count);
|
|
||||||
foreach (var item in m_list)
|
|
||||||
{
|
|
||||||
result.m_list.Add(new DictionaryExpressionDataPair(item.Key, item.Value?.Clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override JToken ToJToken()
|
|
||||||
{
|
|
||||||
var json = new JObject();
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var item in m_list)
|
|
||||||
{
|
|
||||||
json.Add(item.Key, item.Value?.ToJToken() ?? JValue.CreateNull());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean ContainsKey(String key)
|
|
||||||
{
|
|
||||||
return TryGetValue(key, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<String, ExpressionData>> GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return new KeyValuePair<String, ExpressionData>(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return new KeyValuePair<String, ExpressionData>(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IReadOnlyObject.GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return new KeyValuePair<String, Object>(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryGetValue(
|
|
||||||
String key,
|
|
||||||
out ExpressionData value)
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0 &&
|
|
||||||
IndexLookup.TryGetValue(key, out var index))
|
|
||||||
{
|
|
||||||
value = m_list[index].Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Boolean IReadOnlyObject.TryGetValue(
|
|
||||||
String key,
|
|
||||||
out Object value)
|
|
||||||
{
|
|
||||||
if (TryGetValue(key, out ExpressionData data))
|
|
||||||
{
|
|
||||||
value = data;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[OnSerializing]
|
|
||||||
private void OnSerializing(StreamingContext context)
|
|
||||||
{
|
|
||||||
if (m_list?.Count == 0)
|
|
||||||
{
|
|
||||||
m_list = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataContract]
|
|
||||||
private sealed class DictionaryExpressionDataPair
|
|
||||||
{
|
|
||||||
public DictionaryExpressionDataPair(
|
|
||||||
String key,
|
|
||||||
ExpressionData value)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "k")]
|
|
||||||
public readonly String Key;
|
|
||||||
|
|
||||||
[DataMember(Name = "v")]
|
|
||||||
public readonly ExpressionData Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<String, Int32> m_indexLookup;
|
|
||||||
|
|
||||||
[DataMember(Name = "d", EmitDefaultValue = false)]
|
|
||||||
private List<DictionaryExpressionDataPair> m_list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,289 +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 System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
[JsonObject]
|
|
||||||
public class DictionaryExpressionData : ExpressionData, IEnumerable<KeyValuePair<String, ExpressionData>>, IReadOnlyObject
|
|
||||||
{
|
|
||||||
public DictionaryExpressionData()
|
|
||||||
: base(ExpressionDataType.Dictionary)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[IgnoreDataMember]
|
|
||||||
public Int32 Count => m_list?.Count ?? 0;
|
|
||||||
|
|
||||||
[IgnoreDataMember]
|
|
||||||
public IEnumerable<String> Keys
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return pair.Key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[IgnoreDataMember]
|
|
||||||
public IEnumerable<ExpressionData> Values
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return pair.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<Object> IReadOnlyObject.Values
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return pair.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<String, Int32> IndexLookup
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_indexLookup == null)
|
|
||||||
{
|
|
||||||
m_indexLookup = new Dictionary<String, Int32>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < m_list.Count; i++)
|
|
||||||
{
|
|
||||||
var pair = m_list[i];
|
|
||||||
m_indexLookup.Add(pair.Key, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_indexLookup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<DictionaryExpressionDataPair> List
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_list == null)
|
|
||||||
{
|
|
||||||
m_list = new List<DictionaryExpressionDataPair>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExpressionData this[String key]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var index = IndexLookup[key];
|
|
||||||
return m_list[index].Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
// Existing
|
|
||||||
if (IndexLookup.TryGetValue(key, out var index))
|
|
||||||
{
|
|
||||||
key = m_list[index].Key; // preserve casing
|
|
||||||
m_list[index] = new DictionaryExpressionDataPair(key, value);
|
|
||||||
}
|
|
||||||
// New
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Add(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Object IReadOnlyObject.this[String key]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var index = IndexLookup[key];
|
|
||||||
return m_list[index].Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal KeyValuePair<String, ExpressionData> this[Int32 index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var pair = m_list[index];
|
|
||||||
return new KeyValuePair<String, ExpressionData>(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(IEnumerable<KeyValuePair<String, ExpressionData>> pairs)
|
|
||||||
{
|
|
||||||
foreach (var pair in pairs)
|
|
||||||
{
|
|
||||||
Add(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(
|
|
||||||
String key,
|
|
||||||
ExpressionData value)
|
|
||||||
{
|
|
||||||
IndexLookup.Add(key, m_list?.Count ?? 0);
|
|
||||||
List.Add(new DictionaryExpressionDataPair(key, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ExpressionData Clone()
|
|
||||||
{
|
|
||||||
var result = new DictionaryExpressionData();
|
|
||||||
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
result.m_list = new List<DictionaryExpressionDataPair>(m_list.Count);
|
|
||||||
foreach (var item in m_list)
|
|
||||||
{
|
|
||||||
result.m_list.Add(new DictionaryExpressionDataPair(item.Key, item.Value?.Clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override JToken ToJToken()
|
|
||||||
{
|
|
||||||
var json = new JObject();
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var item in m_list)
|
|
||||||
{
|
|
||||||
json.Add(item.Key, item.Value?.ToJToken() ?? JValue.CreateNull());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean ContainsKey(String key)
|
|
||||||
{
|
|
||||||
return TryGetValue(key, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<KeyValuePair<String, ExpressionData>> GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return new KeyValuePair<String, ExpressionData>(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return new KeyValuePair<String, ExpressionData>(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IReadOnlyObject.GetEnumerator()
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var pair in m_list)
|
|
||||||
{
|
|
||||||
yield return new KeyValuePair<String, Object>(pair.Key, pair.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryGetValue(
|
|
||||||
String key,
|
|
||||||
out ExpressionData value)
|
|
||||||
{
|
|
||||||
if (m_list?.Count > 0 &&
|
|
||||||
IndexLookup.TryGetValue(key, out var index))
|
|
||||||
{
|
|
||||||
value = m_list[index].Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Boolean IReadOnlyObject.TryGetValue(
|
|
||||||
String key,
|
|
||||||
out Object value)
|
|
||||||
{
|
|
||||||
if (TryGetValue(key, out ExpressionData data))
|
|
||||||
{
|
|
||||||
value = data;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[OnSerializing]
|
|
||||||
private void OnSerializing(StreamingContext context)
|
|
||||||
{
|
|
||||||
if (m_list?.Count == 0)
|
|
||||||
{
|
|
||||||
m_list = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataContract]
|
|
||||||
private sealed class DictionaryExpressionDataPair
|
|
||||||
{
|
|
||||||
public DictionaryExpressionDataPair(
|
|
||||||
String key,
|
|
||||||
ExpressionData value)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "k")]
|
|
||||||
public readonly String Key;
|
|
||||||
|
|
||||||
[DataMember(Name = "v")]
|
|
||||||
public readonly ExpressionData Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<String, Int32> m_indexLookup;
|
|
||||||
|
|
||||||
[DataMember(Name = "d", EmitDefaultValue = false)]
|
|
||||||
private List<DictionaryExpressionDataPair> m_list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for all template tokens
|
|
||||||
/// </summary>
|
|
||||||
[DataContract]
|
|
||||||
[JsonConverter(typeof(ExpressionDataJsonConverter))]
|
|
||||||
public abstract class ExpressionData
|
|
||||||
{
|
|
||||||
protected ExpressionData(Int32 type)
|
|
||||||
{
|
|
||||||
Type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "t", EmitDefaultValue = false)]
|
|
||||||
internal Int32 Type { get; }
|
|
||||||
|
|
||||||
public abstract ExpressionData Clone();
|
|
||||||
|
|
||||||
public abstract JToken ToJToken();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +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 System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
public static class ExpressionDataExtensions
|
|
||||||
{
|
|
||||||
public static ArrayExpressionData AssertArray(
|
|
||||||
this ExpressionData value,
|
|
||||||
String objectDescription)
|
|
||||||
{
|
|
||||||
if (value is ArrayExpressionData array)
|
|
||||||
{
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(ArrayExpressionData)}' was expected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DictionaryExpressionData AssertDictionary(
|
|
||||||
this ExpressionData value,
|
|
||||||
String objectDescription)
|
|
||||||
{
|
|
||||||
if (value is DictionaryExpressionData dictionary)
|
|
||||||
{
|
|
||||||
return dictionary;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(DictionaryExpressionData)}' was expected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StringExpressionData AssertString(
|
|
||||||
this ExpressionData value,
|
|
||||||
String objectDescription)
|
|
||||||
{
|
|
||||||
if (value is StringExpressionData str)
|
|
||||||
{
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(StringExpressionData)}' was expected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all context data objects (depth first)
|
|
||||||
/// </summary>
|
|
||||||
public static IEnumerable<ExpressionData> Traverse(this ExpressionData value)
|
|
||||||
{
|
|
||||||
return Traverse(value, omitKeys: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns all context data objects (depth first)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="omitKeys">If true, dictionary keys are omitted</param>
|
|
||||||
public static IEnumerable<ExpressionData> Traverse(
|
|
||||||
this ExpressionData value,
|
|
||||||
Boolean omitKeys)
|
|
||||||
{
|
|
||||||
yield return value;
|
|
||||||
|
|
||||||
if (value is ArrayExpressionData || value is DictionaryExpressionData)
|
|
||||||
{
|
|
||||||
var state = new TraversalState(null, value);
|
|
||||||
while (state != null)
|
|
||||||
{
|
|
||||||
if (state.MoveNext(omitKeys))
|
|
||||||
{
|
|
||||||
value = state.Current;
|
|
||||||
yield return value;
|
|
||||||
|
|
||||||
if (value is ArrayExpressionData || value is DictionaryExpressionData)
|
|
||||||
{
|
|
||||||
state = new TraversalState(state, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
state = state.Parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TraversalState
|
|
||||||
{
|
|
||||||
public TraversalState(
|
|
||||||
TraversalState parent,
|
|
||||||
ExpressionData data)
|
|
||||||
{
|
|
||||||
Parent = parent;
|
|
||||||
m_data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean MoveNext(Boolean omitKeys)
|
|
||||||
{
|
|
||||||
switch (m_data.Type)
|
|
||||||
{
|
|
||||||
case ExpressionDataType.Array:
|
|
||||||
var array = m_data.AssertArray("array");
|
|
||||||
if (++m_index < array.Count)
|
|
||||||
{
|
|
||||||
Current = array[m_index];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Current = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
case ExpressionDataType.Dictionary:
|
|
||||||
var dictionary = m_data.AssertDictionary("dictionary");
|
|
||||||
|
|
||||||
// Return the value
|
|
||||||
if (m_isKey)
|
|
||||||
{
|
|
||||||
m_isKey = false;
|
|
||||||
Current = dictionary[m_index].Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (++m_index < dictionary.Count)
|
|
||||||
{
|
|
||||||
// Skip the key, return the value
|
|
||||||
if (omitKeys)
|
|
||||||
{
|
|
||||||
m_isKey = false;
|
|
||||||
Current = dictionary[m_index].Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the key
|
|
||||||
m_isKey = true;
|
|
||||||
Current = new StringExpressionData(dictionary[m_index].Key);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Current = null;
|
|
||||||
return false;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException($"Unexpected {nameof(ExpressionData)} type '{m_data.Type}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExpressionData m_data;
|
|
||||||
private Int32 m_index = -1;
|
|
||||||
private Boolean m_isKey;
|
|
||||||
public ExpressionData Current;
|
|
||||||
public TraversalState Parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,199 +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 System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// JSON serializer for ExpressionData objects
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class ExpressionDataJsonConverter : JsonConverter
|
|
||||||
{
|
|
||||||
public override Boolean CanWrite
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Boolean CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return typeof(ExpressionData).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Object ReadJson(
|
|
||||||
JsonReader reader,
|
|
||||||
Type objectType,
|
|
||||||
Object existingValue,
|
|
||||||
JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
switch (reader.TokenType)
|
|
||||||
{
|
|
||||||
case JsonToken.String:
|
|
||||||
return new StringExpressionData(reader.Value.ToString());
|
|
||||||
|
|
||||||
case JsonToken.Boolean:
|
|
||||||
return new BooleanExpressionData((Boolean)reader.Value);
|
|
||||||
|
|
||||||
case JsonToken.Float:
|
|
||||||
return new NumberExpressionData((Double)reader.Value);
|
|
||||||
|
|
||||||
case JsonToken.Integer:
|
|
||||||
return new NumberExpressionData((Double)(Int64)reader.Value);
|
|
||||||
|
|
||||||
case JsonToken.StartObject:
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Int32? type = null;
|
|
||||||
JObject value = JObject.Load(reader);
|
|
||||||
if (!value.TryGetValue("t", StringComparison.OrdinalIgnoreCase, out JToken typeValue))
|
|
||||||
{
|
|
||||||
type = ExpressionDataType.String;
|
|
||||||
}
|
|
||||||
else if (typeValue.Type == JTokenType.Integer)
|
|
||||||
{
|
|
||||||
type = (Int32)typeValue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return existingValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object newValue = null;
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case ExpressionDataType.String:
|
|
||||||
newValue = new StringExpressionData(null);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ExpressionDataType.Array:
|
|
||||||
newValue = new ArrayExpressionData();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ExpressionDataType.Dictionary:
|
|
||||||
newValue = new DictionaryExpressionData();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ExpressionDataType.Boolean:
|
|
||||||
newValue = new BooleanExpressionData(false);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ExpressionDataType.Number:
|
|
||||||
newValue = new NumberExpressionData(0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ExpressionDataType.CaseSensitiveDictionary:
|
|
||||||
newValue = new CaseSensitiveDictionaryExpressionData();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new NotSupportedException($"Unexpected {nameof(ExpressionDataType)} '{type}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
using JsonReader objectReader = value.CreateReader();
|
|
||||||
serializer.Populate(objectReader, newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WriteJson(
|
|
||||||
JsonWriter writer,
|
|
||||||
Object value,
|
|
||||||
JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
if (Object.ReferenceEquals(value, null))
|
|
||||||
{
|
|
||||||
writer.WriteNull();
|
|
||||||
}
|
|
||||||
else if (value is StringExpressionData stringData)
|
|
||||||
{
|
|
||||||
writer.WriteValue(stringData.Value);
|
|
||||||
}
|
|
||||||
else if (value is BooleanExpressionData boolData)
|
|
||||||
{
|
|
||||||
writer.WriteValue(boolData.Value);
|
|
||||||
}
|
|
||||||
else if (value is NumberExpressionData numberData)
|
|
||||||
{
|
|
||||||
writer.WriteValue(numberData.Value);
|
|
||||||
}
|
|
||||||
else if (value is ArrayExpressionData arrayData)
|
|
||||||
{
|
|
||||||
writer.WriteStartObject();
|
|
||||||
writer.WritePropertyName("t");
|
|
||||||
writer.WriteValue(ExpressionDataType.Array);
|
|
||||||
if (arrayData.Count > 0)
|
|
||||||
{
|
|
||||||
writer.WritePropertyName("a");
|
|
||||||
writer.WriteStartArray();
|
|
||||||
foreach (var item in arrayData)
|
|
||||||
{
|
|
||||||
serializer.Serialize(writer, item);
|
|
||||||
}
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
else if (value is DictionaryExpressionData dictionaryData)
|
|
||||||
{
|
|
||||||
writer.WriteStartObject();
|
|
||||||
writer.WritePropertyName("t");
|
|
||||||
writer.WriteValue(ExpressionDataType.Dictionary);
|
|
||||||
if (dictionaryData.Count > 0)
|
|
||||||
{
|
|
||||||
writer.WritePropertyName("d");
|
|
||||||
writer.WriteStartArray();
|
|
||||||
foreach (var pair in dictionaryData)
|
|
||||||
{
|
|
||||||
writer.WriteStartObject();
|
|
||||||
writer.WritePropertyName("k");
|
|
||||||
writer.WriteValue(pair.Key);
|
|
||||||
writer.WritePropertyName("v");
|
|
||||||
serializer.Serialize(writer, pair.Value);
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
else if (value is CaseSensitiveDictionaryExpressionData caseSensitiveDictionaryData)
|
|
||||||
{
|
|
||||||
writer.WriteStartObject();
|
|
||||||
writer.WritePropertyName("t");
|
|
||||||
writer.WriteValue(ExpressionDataType.CaseSensitiveDictionary);
|
|
||||||
if (caseSensitiveDictionaryData.Count > 0)
|
|
||||||
{
|
|
||||||
writer.WritePropertyName("d");
|
|
||||||
writer.WriteStartArray();
|
|
||||||
foreach (var pair in caseSensitiveDictionaryData)
|
|
||||||
{
|
|
||||||
writer.WriteStartObject();
|
|
||||||
writer.WritePropertyName("k");
|
|
||||||
writer.WriteValue(pair.Key);
|
|
||||||
writer.WritePropertyName("v");
|
|
||||||
serializer.Serialize(writer, pair.Value);
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new NotSupportedException($"Unexpected type '{value.GetType().Name}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
internal static class ExpressionDataType
|
|
||||||
{
|
|
||||||
internal const Int32 String = 0;
|
|
||||||
|
|
||||||
internal const Int32 Array = 1;
|
|
||||||
|
|
||||||
internal const Int32 Dictionary = 2;
|
|
||||||
|
|
||||||
internal const Int32 Boolean = 3;
|
|
||||||
|
|
||||||
internal const Int32 Number = 4;
|
|
||||||
|
|
||||||
internal const Int32 CaseSensitiveDictionary = 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +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 Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
public static class JTokenExtensions
|
|
||||||
{
|
|
||||||
public static ExpressionData ToExpressionData(this JToken value)
|
|
||||||
{
|
|
||||||
return value.ToExpressionData(1, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ExpressionData ToExpressionData(
|
|
||||||
this JToken value,
|
|
||||||
Int32 depth,
|
|
||||||
Int32 maxDepth)
|
|
||||||
{
|
|
||||||
if (depth < maxDepth)
|
|
||||||
{
|
|
||||||
if (value.Type == JTokenType.String)
|
|
||||||
{
|
|
||||||
return new StringExpressionData((String)value);
|
|
||||||
}
|
|
||||||
else if (value.Type == JTokenType.Boolean)
|
|
||||||
{
|
|
||||||
return new BooleanExpressionData((Boolean)value);
|
|
||||||
}
|
|
||||||
else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
|
|
||||||
{
|
|
||||||
return new NumberExpressionData((Double)value);
|
|
||||||
}
|
|
||||||
else if (value.Type == JTokenType.Object)
|
|
||||||
{
|
|
||||||
var subContext = new DictionaryExpressionData();
|
|
||||||
var obj = (JObject)value;
|
|
||||||
foreach (var property in obj.Properties())
|
|
||||||
{
|
|
||||||
subContext[property.Name] = ToExpressionData(property.Value, depth + 1, maxDepth);
|
|
||||||
}
|
|
||||||
return subContext;
|
|
||||||
}
|
|
||||||
else if (value.Type == JTokenType.Array)
|
|
||||||
{
|
|
||||||
var arrayContext = new ArrayExpressionData();
|
|
||||||
var arr = (JArray)value;
|
|
||||||
foreach (var element in arr)
|
|
||||||
{
|
|
||||||
arrayContext.Add(ToExpressionData(element, depth + 1, maxDepth));
|
|
||||||
}
|
|
||||||
return arrayContext;
|
|
||||||
}
|
|
||||||
else if (value.Type == JTokenType.Null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't understand the type or have reached our max, return as string
|
|
||||||
return new StringExpressionData(value.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
public sealed class NumberExpressionData : ExpressionData, INumber
|
|
||||||
{
|
|
||||||
public NumberExpressionData(Double value)
|
|
||||||
: base(ExpressionDataType.Number)
|
|
||||||
{
|
|
||||||
m_value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Double Value
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return m_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ExpressionData Clone()
|
|
||||||
{
|
|
||||||
return new NumberExpressionData(m_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override JToken ToJToken()
|
|
||||||
{
|
|
||||||
if (Double.IsNaN(m_value) || m_value == Double.PositiveInfinity || m_value == Double.NegativeInfinity)
|
|
||||||
{
|
|
||||||
return (JToken)m_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var floored = Math.Floor(m_value);
|
|
||||||
if (m_value == floored && m_value <= (Double)Int32.MaxValue && m_value >= (Double)Int32.MinValue)
|
|
||||||
{
|
|
||||||
var flooredInt = (Int32)floored;
|
|
||||||
return (JToken)flooredInt;
|
|
||||||
}
|
|
||||||
else if (m_value == floored && m_value <= (Double)Int64.MaxValue && m_value >= (Double)Int64.MinValue)
|
|
||||||
{
|
|
||||||
var flooredInt = (Int64)floored;
|
|
||||||
return (JToken)flooredInt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (JToken)m_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override String ToString()
|
|
||||||
{
|
|
||||||
return m_value.ToString("G15", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
Double INumber.GetNumber()
|
|
||||||
{
|
|
||||||
return Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator Double(NumberExpressionData data)
|
|
||||||
{
|
|
||||||
return data.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator NumberExpressionData(Double data)
|
|
||||||
{
|
|
||||||
return new NumberExpressionData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "n", EmitDefaultValue = false)]
|
|
||||||
private Double m_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +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 System.Runtime.Serialization;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Data
|
|
||||||
{
|
|
||||||
[DataContract]
|
|
||||||
public sealed class StringExpressionData : ExpressionData, IString
|
|
||||||
{
|
|
||||||
public StringExpressionData(String value)
|
|
||||||
: base(ExpressionDataType.String)
|
|
||||||
{
|
|
||||||
m_value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String Value
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (m_value == null)
|
|
||||||
{
|
|
||||||
m_value = String.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ExpressionData Clone()
|
|
||||||
{
|
|
||||||
return new StringExpressionData(m_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override JToken ToJToken()
|
|
||||||
{
|
|
||||||
return (JToken)m_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
String IString.GetString()
|
|
||||||
{
|
|
||||||
return Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override String ToString()
|
|
||||||
{
|
|
||||||
return Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator String(StringExpressionData data)
|
|
||||||
{
|
|
||||||
return data.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator StringExpressionData(String data)
|
|
||||||
{
|
|
||||||
return new StringExpressionData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[OnSerializing]
|
|
||||||
private void OnSerializing(StreamingContext context)
|
|
||||||
{
|
|
||||||
if (m_value?.Length == 0)
|
|
||||||
{
|
|
||||||
m_value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataMember(Name = "s", EmitDefaultValue = false)]
|
|
||||||
private String m_value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public sealed class EvaluationOptions
|
|
||||||
{
|
|
||||||
public EvaluationOptions()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public EvaluationOptions(EvaluationOptions copy)
|
|
||||||
{
|
|
||||||
if (copy != null)
|
|
||||||
{
|
|
||||||
MaxMemory = copy.MaxMemory;
|
|
||||||
MaxCacheMemory = copy.MaxCacheMemory;
|
|
||||||
StrictJsonParsing = copy.StrictJsonParsing;
|
|
||||||
AlwaysTraceExpanded = copy.AlwaysTraceExpanded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum memory (in bytes) allowed during expression evaluation.
|
|
||||||
/// Memory is tracked across the entire expression tree evaluation to protect against DOS attacks.
|
|
||||||
/// Default is 1 MB (1048576 bytes) if not specified.
|
|
||||||
/// </summary>
|
|
||||||
public Int32 MaxMemory { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum memory (in bytes) allowed for caching expanded expression results during tracing.
|
|
||||||
/// When exceeded, the cache is cleared and expressions may not be fully expanded in trace output.
|
|
||||||
/// Default is 1 MB (1048576 bytes) if not specified.
|
|
||||||
/// </summary>
|
|
||||||
public Int32 MaxCacheMemory { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to enforce strict JSON parsing in the fromJson function.
|
|
||||||
/// When true, rejects JSON with comments, trailing commas, single quotes, and other non-standard features.
|
|
||||||
/// Default is false if not specified.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean StrictJsonParsing { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether to always include the expanded expression in trace output.
|
|
||||||
/// When true, the expanded expression is always traced even if it matches the original expression or result.
|
|
||||||
/// Default is false if not specified.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean AlwaysTraceExpanded { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,459 +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 System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public sealed class EvaluationResult
|
|
||||||
{
|
|
||||||
internal EvaluationResult(
|
|
||||||
EvaluationContext context,
|
|
||||||
Int32 level,
|
|
||||||
Object val,
|
|
||||||
ValueKind kind,
|
|
||||||
Object raw)
|
|
||||||
: this(context, level, val, kind, raw, false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
internal EvaluationResult(
|
|
||||||
EvaluationContext context,
|
|
||||||
Int32 level,
|
|
||||||
Object val,
|
|
||||||
ValueKind kind,
|
|
||||||
Object raw,
|
|
||||||
Boolean omitTracing)
|
|
||||||
{
|
|
||||||
m_level = level;
|
|
||||||
Value = val;
|
|
||||||
Kind = kind;
|
|
||||||
Raw = raw;
|
|
||||||
m_omitTracing = omitTracing;
|
|
||||||
|
|
||||||
if (!omitTracing)
|
|
||||||
{
|
|
||||||
TraceValue(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueKind Kind { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When an interface converter is applied to the node result, raw contains the original value
|
|
||||||
/// </summary>
|
|
||||||
public Object Raw { get; }
|
|
||||||
|
|
||||||
public Object Value { get; }
|
|
||||||
|
|
||||||
public Boolean IsFalsy
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Null:
|
|
||||||
return true;
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
var boolean = (Boolean)Value;
|
|
||||||
return !boolean;
|
|
||||||
case ValueKind.Number:
|
|
||||||
var number = (Double)Value;
|
|
||||||
return number == 0d || Double.IsNaN(number);
|
|
||||||
case ValueKind.String:
|
|
||||||
var str = (String)Value;
|
|
||||||
return String.Equals(str, String.Empty, StringComparison.Ordinal);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean IsPrimitive => ExpressionUtility.IsPrimitive(Kind);
|
|
||||||
|
|
||||||
public Boolean IsTruthy => !IsFalsy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean AbstractEqual(EvaluationResult right)
|
|
||||||
{
|
|
||||||
return AbstractEqual(Value, right.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean AbstractGreaterThan(EvaluationResult right)
|
|
||||||
{
|
|
||||||
return AbstractGreaterThan(Value, right.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean AbstractGreaterThanOrEqual(EvaluationResult right)
|
|
||||||
{
|
|
||||||
return AbstractEqual(Value, right.Value) || AbstractGreaterThan(Value, right.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean AbstractLessThan(EvaluationResult right)
|
|
||||||
{
|
|
||||||
return AbstractLessThan(Value, right.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean AbstractLessThanOrEqual(EvaluationResult right)
|
|
||||||
{
|
|
||||||
return AbstractEqual(Value, right.Value) || AbstractLessThan(Value, right.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
public Boolean AbstractNotEqual(EvaluationResult right)
|
|
||||||
{
|
|
||||||
return !AbstractEqual(Value, right.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Double ConvertToNumber()
|
|
||||||
{
|
|
||||||
return ConvertToNumber(Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String ConvertToString()
|
|
||||||
{
|
|
||||||
switch (Kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Null:
|
|
||||||
return String.Empty;
|
|
||||||
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
return ((Boolean)Value) ? ExpressionConstants.True : ExpressionConstants.False;
|
|
||||||
|
|
||||||
case ValueKind.Number:
|
|
||||||
if ((Double)Value == -0)
|
|
||||||
{
|
|
||||||
// .NET Core 3.0 now prints negative zero as -0, so we need this to keep out behavior consistent
|
|
||||||
return ((Double)0).ToString(ExpressionConstants.NumberFormat, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
return ((Double)Value).ToString(ExpressionConstants.NumberFormat, CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
return Value as String;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return Kind.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean TryGetCollectionInterface(out Object collection)
|
|
||||||
{
|
|
||||||
if ((Kind == ValueKind.Object || Kind == ValueKind.Array))
|
|
||||||
{
|
|
||||||
var obj = Value;
|
|
||||||
if (obj is IReadOnlyObject)
|
|
||||||
{
|
|
||||||
collection = obj;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (obj is IReadOnlyArray)
|
|
||||||
{
|
|
||||||
collection = obj;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
collection = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Useful for working with values that are not the direct evaluation result of a parameter.
|
|
||||||
/// This allows ExpressionNode authors to leverage the coercion and comparison functions
|
|
||||||
/// for any values.
|
|
||||||
///
|
|
||||||
/// Also note, the value will be canonicalized (for example numeric types converted to double) and any
|
|
||||||
/// matching interfaces applied.
|
|
||||||
/// </summary>
|
|
||||||
public static EvaluationResult CreateIntermediateResult(
|
|
||||||
EvaluationContext context,
|
|
||||||
Object obj)
|
|
||||||
{
|
|
||||||
var val = ExpressionUtility.ConvertToCanonicalValue(obj, out ValueKind kind, out Object raw);
|
|
||||||
return new EvaluationResult(context, 0, val, kind, raw, omitTracing: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceValue(EvaluationContext context)
|
|
||||||
{
|
|
||||||
if (!m_omitTracing)
|
|
||||||
{
|
|
||||||
TraceValue(context, Value, Kind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceValue(
|
|
||||||
EvaluationContext context,
|
|
||||||
Object val,
|
|
||||||
ValueKind kind)
|
|
||||||
{
|
|
||||||
if (!m_omitTracing)
|
|
||||||
{
|
|
||||||
TraceVerbose(context, String.Concat("=> ", ExpressionUtility.FormatValue(context?.SecretMasker, val, kind)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceVerbose(
|
|
||||||
EvaluationContext context,
|
|
||||||
String message)
|
|
||||||
{
|
|
||||||
if (!m_omitTracing)
|
|
||||||
{
|
|
||||||
context?.Trace.Verbose(String.Empty.PadLeft(m_level * 2, '.') + (message ?? String.Empty));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
private static Boolean AbstractEqual(
|
|
||||||
Object canonicalLeftValue,
|
|
||||||
Object canonicalRightValue)
|
|
||||||
{
|
|
||||||
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out var leftKind, out var rightKind);
|
|
||||||
|
|
||||||
// Same kind
|
|
||||||
if (leftKind == rightKind)
|
|
||||||
{
|
|
||||||
switch (leftKind)
|
|
||||||
{
|
|
||||||
// Null, Null
|
|
||||||
case ValueKind.Null:
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Number, Number
|
|
||||||
case ValueKind.Number:
|
|
||||||
var leftDouble = (Double)canonicalLeftValue;
|
|
||||||
var rightDouble = (Double)canonicalRightValue;
|
|
||||||
if (Double.IsNaN(leftDouble) || Double.IsNaN(rightDouble))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return leftDouble == rightDouble;
|
|
||||||
|
|
||||||
// String, String
|
|
||||||
case ValueKind.String:
|
|
||||||
var leftString = (String)canonicalLeftValue;
|
|
||||||
var rightString = (String)canonicalRightValue;
|
|
||||||
return String.Equals(leftString, rightString, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// Boolean, Boolean
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
var leftBoolean = (Boolean)canonicalLeftValue;
|
|
||||||
var rightBoolean = (Boolean)canonicalRightValue;
|
|
||||||
return leftBoolean == rightBoolean;
|
|
||||||
|
|
||||||
// Object, Object
|
|
||||||
case ValueKind.Object:
|
|
||||||
case ValueKind.Array:
|
|
||||||
return Object.ReferenceEquals(canonicalLeftValue, canonicalRightValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
private static Boolean AbstractGreaterThan(
|
|
||||||
Object canonicalLeftValue,
|
|
||||||
Object canonicalRightValue)
|
|
||||||
{
|
|
||||||
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out var leftKind, out var rightKind);
|
|
||||||
|
|
||||||
// Same kind
|
|
||||||
if (leftKind == rightKind)
|
|
||||||
{
|
|
||||||
switch (leftKind)
|
|
||||||
{
|
|
||||||
// Number, Number
|
|
||||||
case ValueKind.Number:
|
|
||||||
var leftDouble = (Double)canonicalLeftValue;
|
|
||||||
var rightDouble = (Double)canonicalRightValue;
|
|
||||||
if (Double.IsNaN(leftDouble) || Double.IsNaN(rightDouble))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return leftDouble > rightDouble;
|
|
||||||
|
|
||||||
// String, String
|
|
||||||
case ValueKind.String:
|
|
||||||
var leftString = (String)canonicalLeftValue;
|
|
||||||
var rightString = (String)canonicalRightValue;
|
|
||||||
return String.Compare(leftString, rightString, StringComparison.OrdinalIgnoreCase) > 0;
|
|
||||||
|
|
||||||
// Boolean, Boolean
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
var leftBoolean = (Boolean)canonicalLeftValue;
|
|
||||||
var rightBoolean = (Boolean)canonicalRightValue;
|
|
||||||
return leftBoolean && !rightBoolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
|
|
||||||
/// </summary>
|
|
||||||
private static Boolean AbstractLessThan(
|
|
||||||
Object canonicalLeftValue,
|
|
||||||
Object canonicalRightValue)
|
|
||||||
{
|
|
||||||
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out var leftKind, out var rightKind);
|
|
||||||
|
|
||||||
// Same kind
|
|
||||||
if (leftKind == rightKind)
|
|
||||||
{
|
|
||||||
switch (leftKind)
|
|
||||||
{
|
|
||||||
// Number, Number
|
|
||||||
case ValueKind.Number:
|
|
||||||
var leftDouble = (Double)canonicalLeftValue;
|
|
||||||
var rightDouble = (Double)canonicalRightValue;
|
|
||||||
if (Double.IsNaN(leftDouble) || Double.IsNaN(rightDouble))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return leftDouble < rightDouble;
|
|
||||||
|
|
||||||
// String, String
|
|
||||||
case ValueKind.String:
|
|
||||||
var leftString = (String)canonicalLeftValue;
|
|
||||||
var rightString = (String)canonicalRightValue;
|
|
||||||
return String.Compare(leftString, rightString, StringComparison.OrdinalIgnoreCase) < 0;
|
|
||||||
|
|
||||||
// Boolean, Boolean
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
var leftBoolean = (Boolean)canonicalLeftValue;
|
|
||||||
var rightBoolean = (Boolean)canonicalRightValue;
|
|
||||||
return !leftBoolean && rightBoolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
|
||||||
/// Except objects are not coerced to primitives.
|
|
||||||
private static void CoerceTypes(
|
|
||||||
ref Object canonicalLeftValue,
|
|
||||||
ref Object canonicalRightValue,
|
|
||||||
out ValueKind leftKind,
|
|
||||||
out ValueKind rightKind)
|
|
||||||
{
|
|
||||||
leftKind = GetKind(canonicalLeftValue);
|
|
||||||
rightKind = GetKind(canonicalRightValue);
|
|
||||||
|
|
||||||
// Same kind
|
|
||||||
if (leftKind == rightKind)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
// Number, String
|
|
||||||
else if (leftKind == ValueKind.Number && rightKind == ValueKind.String)
|
|
||||||
{
|
|
||||||
canonicalRightValue = ConvertToNumber(canonicalRightValue);
|
|
||||||
rightKind = ValueKind.Number;
|
|
||||||
}
|
|
||||||
// String, Number
|
|
||||||
else if (leftKind == ValueKind.String && rightKind == ValueKind.Number)
|
|
||||||
{
|
|
||||||
canonicalLeftValue = ConvertToNumber(canonicalLeftValue);
|
|
||||||
leftKind = ValueKind.Number;
|
|
||||||
}
|
|
||||||
// Boolean|Null, Any
|
|
||||||
else if (leftKind == ValueKind.Boolean || leftKind == ValueKind.Null)
|
|
||||||
{
|
|
||||||
canonicalLeftValue = ConvertToNumber(canonicalLeftValue);
|
|
||||||
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out leftKind, out rightKind);
|
|
||||||
}
|
|
||||||
// Any, Boolean|Null
|
|
||||||
else if (rightKind == ValueKind.Boolean || rightKind == ValueKind.Null)
|
|
||||||
{
|
|
||||||
canonicalRightValue = ConvertToNumber(canonicalRightValue);
|
|
||||||
CoerceTypes(ref canonicalLeftValue, ref canonicalRightValue, out leftKind, out rightKind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// For primitives, follows the Javascript rules (the Number function in Javascript). Otherwise NaN.
|
|
||||||
/// </summary>
|
|
||||||
private static Double ConvertToNumber(Object canonicalValue)
|
|
||||||
{
|
|
||||||
var kind = GetKind(canonicalValue);
|
|
||||||
switch (kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Null:
|
|
||||||
return 0d;
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
return (Boolean)canonicalValue ? 1d : 0d;
|
|
||||||
case ValueKind.Number:
|
|
||||||
return (Double)canonicalValue;
|
|
||||||
case ValueKind.String:
|
|
||||||
return ExpressionUtility.ParseNumber(canonicalValue as String);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Double.NaN;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ValueKind GetKind(Object canonicalValue)
|
|
||||||
{
|
|
||||||
if (Object.ReferenceEquals(canonicalValue, null))
|
|
||||||
{
|
|
||||||
return ValueKind.Null;
|
|
||||||
}
|
|
||||||
else if (canonicalValue is Boolean)
|
|
||||||
{
|
|
||||||
return ValueKind.Boolean;
|
|
||||||
}
|
|
||||||
else if (canonicalValue is Double)
|
|
||||||
{
|
|
||||||
return ValueKind.Number;
|
|
||||||
}
|
|
||||||
else if (canonicalValue is String)
|
|
||||||
{
|
|
||||||
return ValueKind.String;
|
|
||||||
}
|
|
||||||
else if (canonicalValue is IReadOnlyObject)
|
|
||||||
{
|
|
||||||
return ValueKind.Object;
|
|
||||||
}
|
|
||||||
else if (canonicalValue is IReadOnlyArray)
|
|
||||||
{
|
|
||||||
return ValueKind.Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValueKind.Object;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Int32 m_level;
|
|
||||||
private readonly Boolean m_omitTracing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using GitHub.Actions.Expressions.Sdk.Functions;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public static class ExpressionConstants
|
|
||||||
{
|
|
||||||
static ExpressionConstants()
|
|
||||||
{
|
|
||||||
AddFunction<Contains>("contains", 2, 2);
|
|
||||||
AddFunction<EndsWith>("endsWith", 2, 2);
|
|
||||||
AddFunction<Format>("format", 1, Byte.MaxValue);
|
|
||||||
AddFunction<Join>("join", 1, 2);
|
|
||||||
AddFunction<StartsWith>("startsWith", 2, 2);
|
|
||||||
AddFunction<ToJson>("toJson", 1, 1);
|
|
||||||
AddFunction<FromJson>("fromJson", 1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddFunction<T>(String name, Int32 minParameters, Int32 maxParameters)
|
|
||||||
where T : Function, new()
|
|
||||||
{
|
|
||||||
s_wellKnownFunctions.Add(name, new FunctionInfo<T>(name, minParameters, maxParameters));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static readonly String False = "false";
|
|
||||||
internal static readonly String Infinity = "Infinity";
|
|
||||||
internal static readonly Int32 MaxDepth = 50;
|
|
||||||
internal static readonly Int32 MaxLength = 21000; // Under 85,000 large object heap threshold, even if .NET switches to UTF-32
|
|
||||||
internal static readonly String NaN = "NaN";
|
|
||||||
internal static readonly String NegativeInfinity = "-Infinity";
|
|
||||||
public static readonly String Null = "null";
|
|
||||||
internal static readonly String NumberFormat = "G15";
|
|
||||||
internal static readonly String True = "true";
|
|
||||||
private static readonly Dictionary<String, IFunctionInfo> s_wellKnownFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public static readonly IReadOnlyDictionary<String, IFunctionInfo> WellKnownFunctions = new ReadOnlyDictionary<String, IFunctionInfo>(s_wellKnownFunctions);
|
|
||||||
|
|
||||||
// Punctuation
|
|
||||||
internal const Char StartGroup = '('; // logical grouping
|
|
||||||
internal const Char StartIndex = '[';
|
|
||||||
public static readonly Char StartParameter = '('; // function call
|
|
||||||
internal const Char EndGroup = ')'; // logical grouping
|
|
||||||
internal const Char EndIndex = ']';
|
|
||||||
public static readonly Char EndParameter = ')'; // function calll
|
|
||||||
internal const Char Separator = ',';
|
|
||||||
internal const Char Dereference = '.';
|
|
||||||
internal const Char Wildcard = '*';
|
|
||||||
|
|
||||||
// Operators
|
|
||||||
internal const String Not = "!";
|
|
||||||
internal const String NotEqual = "!=";
|
|
||||||
internal const String GreaterThan = ">";
|
|
||||||
internal const String GreaterThanOrEqual = ">=";
|
|
||||||
internal const String LessThan = "<";
|
|
||||||
internal const String LessThanOrEqual = "<=";
|
|
||||||
internal const String Equal = "==";
|
|
||||||
internal const String And = "&&";
|
|
||||||
internal const String Or = "||";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public class ExpressionException : Exception
|
|
||||||
{
|
|
||||||
internal ExpressionException(ISecretMasker secretMasker, String message)
|
|
||||||
{
|
|
||||||
if (secretMasker != null)
|
|
||||||
{
|
|
||||||
message = secretMasker.MaskSecrets(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override String Message => m_message;
|
|
||||||
|
|
||||||
private readonly String m_message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,471 +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 System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using GitHub.Actions.Expressions.Sdk.Operators;
|
|
||||||
using GitHub.Actions.Expressions.Tokens;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using GitHub.Actions.Expressions.Sdk.Functions;
|
|
||||||
|
|
||||||
public sealed class ExpressionParser
|
|
||||||
{
|
|
||||||
public IExpressionNode CreateTree(
|
|
||||||
String expression,
|
|
||||||
ITraceWriter trace,
|
|
||||||
IEnumerable<INamedValueInfo> namedValues,
|
|
||||||
IEnumerable<IFunctionInfo> functions)
|
|
||||||
{
|
|
||||||
var context = new ParseContext(expression, trace, namedValues, functions);
|
|
||||||
context.Trace.Info($"Parsing expression: <{expression}>");
|
|
||||||
return CreateTree(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IExpressionNode ValidateSyntax(
|
|
||||||
String expression,
|
|
||||||
ITraceWriter trace)
|
|
||||||
{
|
|
||||||
var context = new ParseContext(expression, trace, namedValues: null, functions: null, allowUnknownKeywords: true);
|
|
||||||
context.Trace.Info($"Validating expression syntax: <{expression}>");
|
|
||||||
return CreateTree(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IExpressionNode CreateTree(ParseContext context)
|
|
||||||
{
|
|
||||||
// Push the tokens
|
|
||||||
while (context.LexicalAnalyzer.TryGetNextToken(ref context.Token))
|
|
||||||
{
|
|
||||||
// Unexpected
|
|
||||||
if (context.Token.Kind == TokenKind.Unexpected)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedSymbol, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
// Operator
|
|
||||||
else if (context.Token.IsOperator)
|
|
||||||
{
|
|
||||||
PushOperator(context);
|
|
||||||
}
|
|
||||||
// Operand
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PushOperand(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.LastToken = context.Token;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No tokens
|
|
||||||
if (context.LastToken == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check unexpected end of expression
|
|
||||||
if (context.Operators.Count > 0)
|
|
||||||
{
|
|
||||||
var unexpectedLastToken = false;
|
|
||||||
switch (context.LastToken.Kind)
|
|
||||||
{
|
|
||||||
case TokenKind.EndGroup: // ")" logical grouping
|
|
||||||
case TokenKind.EndIndex: // "]"
|
|
||||||
case TokenKind.EndParameters: // ")" function call
|
|
||||||
// Legal
|
|
||||||
break;
|
|
||||||
case TokenKind.Function:
|
|
||||||
// Illegal
|
|
||||||
unexpectedLastToken = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
unexpectedLastToken = context.LastToken.IsOperator;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unexpectedLastToken || context.LexicalAnalyzer.UnclosedTokens.Any())
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnexpectedEndOfExpression, context.LastToken, context.Expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush operators
|
|
||||||
while (context.Operators.Count > 0)
|
|
||||||
{
|
|
||||||
FlushTopOperator(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check max depth
|
|
||||||
var result = context.Operands.Single();
|
|
||||||
CheckMaxDepth(context, result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PushOperand(ParseContext context)
|
|
||||||
{
|
|
||||||
// Create the node
|
|
||||||
var node = default(ExpressionNode);
|
|
||||||
switch (context.Token.Kind)
|
|
||||||
{
|
|
||||||
// Function
|
|
||||||
case TokenKind.Function:
|
|
||||||
var function = context.Token.RawValue;
|
|
||||||
if (TryGetFunctionInfo(context, function, out var functionInfo))
|
|
||||||
{
|
|
||||||
node = functionInfo.CreateNode();
|
|
||||||
node.Name = function;
|
|
||||||
}
|
|
||||||
else if (context.AllowUnknownKeywords)
|
|
||||||
{
|
|
||||||
node = new NoOperation();
|
|
||||||
node.Name = function;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnrecognizedFunction, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Named-value
|
|
||||||
case TokenKind.NamedValue:
|
|
||||||
var name = context.Token.RawValue;
|
|
||||||
if (context.ExtensionNamedValues.TryGetValue(name, out var namedValueInfo))
|
|
||||||
{
|
|
||||||
node = namedValueInfo.CreateNode();
|
|
||||||
node.Name = name;
|
|
||||||
|
|
||||||
}
|
|
||||||
else if (context.AllowUnknownKeywords)
|
|
||||||
{
|
|
||||||
node = new NoOperationNamedValue();
|
|
||||||
node.Name = name;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.UnrecognizedNamedValue, context.Token, context.Expression);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Otherwise simple
|
|
||||||
default:
|
|
||||||
node = context.Token.ToNode();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the operand
|
|
||||||
context.Operands.Push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PushOperator(ParseContext context)
|
|
||||||
{
|
|
||||||
// Flush higher or equal precedence
|
|
||||||
if (context.Token.Associativity == Associativity.LeftToRight)
|
|
||||||
{
|
|
||||||
var precedence = context.Token.Precedence;
|
|
||||||
while (context.Operators.Count > 0)
|
|
||||||
{
|
|
||||||
var topOperator = context.Operators.Peek();
|
|
||||||
if (precedence <= topOperator.Precedence &&
|
|
||||||
topOperator.Kind != TokenKind.StartGroup && // Unless top is "(" logical grouping
|
|
||||||
topOperator.Kind != TokenKind.StartIndex && // or unless top is "["
|
|
||||||
topOperator.Kind != TokenKind.StartParameters &&// or unless top is "(" function call
|
|
||||||
topOperator.Kind != TokenKind.Separator) // or unless top is ","
|
|
||||||
{
|
|
||||||
FlushTopOperator(context);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the operator
|
|
||||||
context.Operators.Push(context.Token);
|
|
||||||
|
|
||||||
// Process closing operators now, since context.LastToken is required
|
|
||||||
// to accurately process TokenKind.EndParameters
|
|
||||||
switch (context.Token.Kind)
|
|
||||||
{
|
|
||||||
case TokenKind.EndGroup: // ")" logical grouping
|
|
||||||
case TokenKind.EndIndex: // "]"
|
|
||||||
case TokenKind.EndParameters: // ")" function call
|
|
||||||
FlushTopOperator(context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void FlushTopOperator(ParseContext context)
|
|
||||||
{
|
|
||||||
// Special handling for closing operators
|
|
||||||
switch (context.Operators.Peek().Kind)
|
|
||||||
{
|
|
||||||
case TokenKind.EndIndex: // "]"
|
|
||||||
FlushTopEndIndex(context);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case TokenKind.EndGroup: // ")" logical grouping
|
|
||||||
FlushTopEndGroup(context);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case TokenKind.EndParameters: // ")" function call
|
|
||||||
FlushTopEndParameters(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop the operator
|
|
||||||
var @operator = context.Operators.Pop();
|
|
||||||
|
|
||||||
// Create the node
|
|
||||||
var node = (Container)@operator.ToNode();
|
|
||||||
|
|
||||||
// Pop the operands, add to the node
|
|
||||||
var operands = PopOperands(context, @operator.OperandCount);
|
|
||||||
foreach (var operand in operands)
|
|
||||||
{
|
|
||||||
// Flatten nested And
|
|
||||||
if (node is And)
|
|
||||||
{
|
|
||||||
if (operand is And nestedAnd)
|
|
||||||
{
|
|
||||||
foreach (var nestedParameter in nestedAnd.Parameters)
|
|
||||||
{
|
|
||||||
node.AddParameter(nestedParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Flatten nested Or
|
|
||||||
else if (node is Or)
|
|
||||||
{
|
|
||||||
if (operand is Or nestedOr)
|
|
||||||
{
|
|
||||||
foreach (var nestedParameter in nestedOr.Parameters)
|
|
||||||
{
|
|
||||||
node.AddParameter(nestedParameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
node.AddParameter(operand);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the node to the operand stack
|
|
||||||
context.Operands.Push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Flushes the ")" logical grouping operator
|
|
||||||
/// </summary>
|
|
||||||
private static void FlushTopEndGroup(ParseContext context)
|
|
||||||
{
|
|
||||||
// Pop the operators
|
|
||||||
PopOperator(context, TokenKind.EndGroup); // ")" logical grouping
|
|
||||||
PopOperator(context, TokenKind.StartGroup); // "(" logical grouping
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Flushes the "]" operator
|
|
||||||
/// </summary>
|
|
||||||
private static void FlushTopEndIndex(ParseContext context)
|
|
||||||
{
|
|
||||||
// Pop the operators
|
|
||||||
PopOperator(context, TokenKind.EndIndex); // "]"
|
|
||||||
var @operator = PopOperator(context, TokenKind.StartIndex); // "["
|
|
||||||
|
|
||||||
// Create the node
|
|
||||||
var node = (Container)@operator.ToNode();
|
|
||||||
|
|
||||||
// Pop the operands, add to the node
|
|
||||||
var operands = PopOperands(context, @operator.OperandCount);
|
|
||||||
foreach (var operand in operands)
|
|
||||||
{
|
|
||||||
node.AddParameter(operand);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the node to the operand stack
|
|
||||||
context.Operands.Push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ")" function call
|
|
||||||
private static void FlushTopEndParameters(ParseContext context)
|
|
||||||
{
|
|
||||||
// Pop the operator
|
|
||||||
var @operator = PopOperator(context, TokenKind.EndParameters); // ")" function call
|
|
||||||
|
|
||||||
// Sanity check top operator is the current token
|
|
||||||
if (!Object.ReferenceEquals(@operator, context.Token))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Expected the operator to be the current token");
|
|
||||||
}
|
|
||||||
|
|
||||||
var function = default(Function);
|
|
||||||
|
|
||||||
// No parameters
|
|
||||||
if (context.LastToken.Kind == TokenKind.StartParameters)
|
|
||||||
{
|
|
||||||
// Node already exists on the operand stack
|
|
||||||
function = (Function)context.Operands.Peek();
|
|
||||||
}
|
|
||||||
// Has parameters
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Pop the operands
|
|
||||||
var parameterCount = 1;
|
|
||||||
while (context.Operators.Peek().Kind == TokenKind.Separator)
|
|
||||||
{
|
|
||||||
parameterCount++;
|
|
||||||
context.Operators.Pop();
|
|
||||||
}
|
|
||||||
var functionOperands = PopOperands(context, parameterCount);
|
|
||||||
|
|
||||||
// Node already exists on the operand stack
|
|
||||||
function = (Function)context.Operands.Peek();
|
|
||||||
|
|
||||||
// Add the operands to the node
|
|
||||||
foreach (var operand in functionOperands)
|
|
||||||
{
|
|
||||||
function.AddParameter(operand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop the "(" operator too
|
|
||||||
@operator = PopOperator(context, TokenKind.StartParameters);
|
|
||||||
|
|
||||||
// Check min/max parameter count
|
|
||||||
TryGetFunctionInfo(context, function.Name, out var functionInfo);
|
|
||||||
if (functionInfo == null && context.AllowUnknownKeywords)
|
|
||||||
{
|
|
||||||
// Don't check min/max
|
|
||||||
}
|
|
||||||
else if (function.Parameters.Count < functionInfo.MinParameters)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.TooFewParameters, token: @operator, expression: context.Expression);
|
|
||||||
}
|
|
||||||
else if (function.Parameters.Count > functionInfo.MaxParameters)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.TooManyParameters, token: @operator, expression: context.Expression);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pops N operands from the operand stack. The operands are returned
|
|
||||||
/// in their natural listed order, i.e. not last-in-first-out.
|
|
||||||
/// </summary>
|
|
||||||
private static List<ExpressionNode> PopOperands(
|
|
||||||
ParseContext context,
|
|
||||||
Int32 count)
|
|
||||||
{
|
|
||||||
var result = new List<ExpressionNode>();
|
|
||||||
while (count-- > 0)
|
|
||||||
{
|
|
||||||
result.Add(context.Operands.Pop());
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Reverse();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pops an operator and asserts it is the expected kind.
|
|
||||||
/// </summary>
|
|
||||||
private static Token PopOperator(
|
|
||||||
ParseContext context,
|
|
||||||
TokenKind expected)
|
|
||||||
{
|
|
||||||
var token = context.Operators.Pop();
|
|
||||||
if (token.Kind != expected)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException($"Expected operator '{expected}' to be popped. Actual '{token.Kind}'.");
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks the max depth of the expression tree
|
|
||||||
/// </summary>
|
|
||||||
private static void CheckMaxDepth(
|
|
||||||
ParseContext context,
|
|
||||||
ExpressionNode node,
|
|
||||||
Int32 depth = 1)
|
|
||||||
{
|
|
||||||
if (depth > ExpressionConstants.MaxDepth)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.ExceededMaxDepth, token: null, expression: context.Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node is Container container)
|
|
||||||
{
|
|
||||||
foreach (var parameter in container.Parameters)
|
|
||||||
{
|
|
||||||
CheckMaxDepth(context, parameter, depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Boolean TryGetFunctionInfo(
|
|
||||||
ParseContext context,
|
|
||||||
String name,
|
|
||||||
out IFunctionInfo functionInfo)
|
|
||||||
{
|
|
||||||
return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) ||
|
|
||||||
context.ExtensionFunctions.TryGetValue(name, out functionInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ParseContext
|
|
||||||
{
|
|
||||||
public Boolean AllowUnknownKeywords;
|
|
||||||
public readonly String Expression;
|
|
||||||
public readonly Dictionary<String, IFunctionInfo> ExtensionFunctions = new Dictionary<String, IFunctionInfo>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public readonly Dictionary<String, INamedValueInfo> ExtensionNamedValues = new Dictionary<String, INamedValueInfo>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public readonly LexicalAnalyzer LexicalAnalyzer;
|
|
||||||
public readonly Stack<ExpressionNode> Operands = new Stack<ExpressionNode>();
|
|
||||||
public readonly Stack<Token> Operators = new Stack<Token>();
|
|
||||||
public readonly ITraceWriter Trace;
|
|
||||||
public Token Token;
|
|
||||||
public Token LastToken;
|
|
||||||
|
|
||||||
public ParseContext(
|
|
||||||
String expression,
|
|
||||||
ITraceWriter trace,
|
|
||||||
IEnumerable<INamedValueInfo> namedValues,
|
|
||||||
IEnumerable<IFunctionInfo> functions,
|
|
||||||
Boolean allowUnknownKeywords = false)
|
|
||||||
{
|
|
||||||
Expression = expression ?? String.Empty;
|
|
||||||
if (Expression.Length > ExpressionConstants.MaxLength)
|
|
||||||
{
|
|
||||||
throw new ParseException(ParseExceptionKind.ExceededMaxLength, token: null, expression: Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
Trace = trace ?? new NoOperationTraceWriter();
|
|
||||||
foreach (var namedValueInfo in (namedValues ?? new INamedValueInfo[0]))
|
|
||||||
{
|
|
||||||
ExtensionNamedValues.Add(namedValueInfo.Name, namedValueInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var functionInfo in (functions ?? new IFunctionInfo[0]))
|
|
||||||
{
|
|
||||||
ExtensionFunctions.Add(functionInfo.Name, functionInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
LexicalAnalyzer = new LexicalAnalyzer(Expression);
|
|
||||||
AllowUnknownKeywords = allowUnknownKeywords;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class NoOperationTraceWriter : ITraceWriter
|
|
||||||
{
|
|
||||||
public void Info(String message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Verbose(String message)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using System;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public class FunctionInfo<T> : IFunctionInfo
|
|
||||||
where T : Function, new()
|
|
||||||
{
|
|
||||||
public FunctionInfo(String name, Int32 minParameters, Int32 maxParameters)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
MinParameters = minParameters;
|
|
||||||
MaxParameters = maxParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String Name { get; }
|
|
||||||
|
|
||||||
public Int32 MinParameters { get; }
|
|
||||||
|
|
||||||
public Int32 MaxParameters { get; }
|
|
||||||
|
|
||||||
public Function CreateNode()
|
|
||||||
{
|
|
||||||
return new T();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public interface IExpressionNode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the expression and returns the result, wrapped in a helper
|
|
||||||
/// for converting, comparing, and traversing objects.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="trace">Optional trace writer</param>
|
|
||||||
/// <param name="secretMasker">Optional secret masker</param>
|
|
||||||
/// <param name="state">State object for custom evaluation function nodes and custom named-value nodes</param>
|
|
||||||
/// <param name="options">Evaluation options</param>
|
|
||||||
EvaluationResult Evaluate(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker? secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,320 +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 System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
using Index = GitHub.Actions.Expressions.Sdk.Operators.Index;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public static class IExpressionNodeExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the node and all descendant nodes
|
|
||||||
/// </summary>
|
|
||||||
public static IEnumerable<IExpressionNode> Traverse(this IExpressionNode node)
|
|
||||||
{
|
|
||||||
yield return node;
|
|
||||||
|
|
||||||
if (node is Container container && container.Parameters.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var parameter in container.Parameters)
|
|
||||||
{
|
|
||||||
foreach (var descendant in parameter.Traverse())
|
|
||||||
{
|
|
||||||
yield return descendant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether specific contexts or sub-properties of contexts are referenced.
|
|
||||||
/// If a conclusive determination cannot be made, then the pattern is considered matched.
|
|
||||||
/// For example, the expression "toJson(github)" matches the pattern "github.event" because
|
|
||||||
/// the value is passed to a function. Not enough information is known to determine whether
|
|
||||||
/// the function requires the sub-property. Therefore, assume it is required.
|
|
||||||
///
|
|
||||||
/// Patterns may contain wildcards to match any literal. For example, the pattern
|
|
||||||
/// "needs.*.outputs" will produce a match for the expression "needs.my-job.outputs.my-output".
|
|
||||||
/// </summary>
|
|
||||||
public static Boolean[] CheckReferencesContext(
|
|
||||||
this IExpressionNode tree,
|
|
||||||
params String[] patterns)
|
|
||||||
{
|
|
||||||
// The result is an array of booleans, one per pattern
|
|
||||||
var result = new Boolean[patterns.Length];
|
|
||||||
|
|
||||||
// Stores the match segments for each pattern. For example
|
|
||||||
// the patterns [ "github.event", "needs.*.outputs" ] would
|
|
||||||
// be stored as:
|
|
||||||
// [
|
|
||||||
// [
|
|
||||||
// NamedValue:github
|
|
||||||
// Literal:"event"
|
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// NamedValue:needs
|
|
||||||
// Wildcard:*
|
|
||||||
// Literal:"outputs"
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
var segmentedPatterns = default(Stack<IExpressionNode>[]);
|
|
||||||
|
|
||||||
// Walk the expression tree
|
|
||||||
var stack = new Stack<IExpressionNode>();
|
|
||||||
stack.Push(tree);
|
|
||||||
while (stack.Count > 0)
|
|
||||||
{
|
|
||||||
var node = stack.Pop();
|
|
||||||
|
|
||||||
// Attempt to match a named-value or index operator.
|
|
||||||
// Note, when entering this block, descendant nodes are only pushed
|
|
||||||
// to the stack for further processing under special conditions.
|
|
||||||
if (node is NamedValue || node is Index)
|
|
||||||
{
|
|
||||||
// Lazy initialize the pattern segments
|
|
||||||
if (segmentedPatterns is null)
|
|
||||||
{
|
|
||||||
segmentedPatterns = new Stack<IExpressionNode>[patterns.Length];
|
|
||||||
var parser = new ExpressionParser();
|
|
||||||
for (var i = 0; i < patterns.Length; i++)
|
|
||||||
{
|
|
||||||
var pattern = patterns[i];
|
|
||||||
var patternTree = parser.ValidateSyntax(pattern, null);
|
|
||||||
var patternSegments = GetMatchSegments(patternTree, out _);
|
|
||||||
if (patternSegments.Count == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Invalid context-match-pattern '{pattern}'");
|
|
||||||
}
|
|
||||||
segmentedPatterns[i] = patternSegments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match
|
|
||||||
Match(node, segmentedPatterns, result, out var needsFurtherAnalysis);
|
|
||||||
|
|
||||||
// Push nested nodes that need further analysis
|
|
||||||
if (needsFurtherAnalysis?.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var nestedNode in needsFurtherAnalysis)
|
|
||||||
{
|
|
||||||
stack.Push(nestedNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Push children of any other container node.
|
|
||||||
else if (node is Container container && container.Parameters.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var child in container.Parameters)
|
|
||||||
{
|
|
||||||
stack.Push(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempts to match a node within a user-provided-expression against a set of patterns.
|
|
||||||
//
|
|
||||||
// For example consider the user-provided-expression "github.event.base_ref || github.event.before"
|
|
||||||
// The Match method would be called twice, once for the sub-expression "github.event.base_ref" and
|
|
||||||
// once for the sub-expression "github.event.before".
|
|
||||||
private static void Match(
|
|
||||||
IExpressionNode node,
|
|
||||||
Stack<IExpressionNode>[] patterns,
|
|
||||||
Boolean[] result,
|
|
||||||
out List<ExpressionNode> needsFurtherAnalysis)
|
|
||||||
{
|
|
||||||
var nodeSegments = GetMatchSegments(node, out needsFurtherAnalysis);
|
|
||||||
|
|
||||||
if (nodeSegments.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeNamedValue = nodeSegments.Peek() as NamedValue;
|
|
||||||
var originalNodeSegments = nodeSegments;
|
|
||||||
|
|
||||||
for (var i = 0; i < patterns.Length; i++)
|
|
||||||
{
|
|
||||||
var patternSegments = patterns[i];
|
|
||||||
var patternNamedValue = patternSegments.Peek() as NamedValue;
|
|
||||||
|
|
||||||
// Compare the named-value
|
|
||||||
if (String.Equals(nodeNamedValue.Name, patternNamedValue.Name, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// Clone the stacks before mutating
|
|
||||||
nodeSegments = new Stack<IExpressionNode>(originalNodeSegments.Reverse()); // Push reverse to preserve order
|
|
||||||
nodeSegments.Pop();
|
|
||||||
patternSegments = new Stack<IExpressionNode>(patternSegments.Reverse()); // Push reverse to preserve order
|
|
||||||
patternSegments.Pop();
|
|
||||||
|
|
||||||
// Walk the stacks
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
// Every pattern segment was matched
|
|
||||||
if (patternSegments.Count == 0)
|
|
||||||
{
|
|
||||||
result[i] = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Every node segment was matched. Treat the pattern as matched. There is not
|
|
||||||
// enough information to determine whether the property is required; assume it is.
|
|
||||||
// For example, consider the pattern "github.event" and the expression "toJson(github)".
|
|
||||||
// In this example the function requires the full structure of the named-value.
|
|
||||||
else if (nodeSegments.Count == 0)
|
|
||||||
{
|
|
||||||
result[i] = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeSegment = nodeSegments.Pop();
|
|
||||||
var patternSegment = patternSegments.Pop();
|
|
||||||
|
|
||||||
// The behavior of a wildcard varies depending on whether the left operand
|
|
||||||
// is an array or an object. For simplicity, treat the pattern as matched.
|
|
||||||
if (nodeSegment is Wildcard)
|
|
||||||
{
|
|
||||||
result[i] = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Treat a wildcard pattern segment as matching any literal segment
|
|
||||||
else if (patternSegment is Wildcard)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert literals to string and compare
|
|
||||||
var nodeLiteral = nodeSegment as Literal;
|
|
||||||
var nodeEvaluationResult = EvaluationResult.CreateIntermediateResult(null, nodeLiteral.Value);
|
|
||||||
var nodeString = nodeEvaluationResult.ConvertToString();
|
|
||||||
var patternLiteral = patternSegment as Literal;
|
|
||||||
var patternEvaluationResult = EvaluationResult.CreateIntermediateResult(null, patternLiteral.Value);
|
|
||||||
var patternString = patternEvaluationResult.ConvertToString();
|
|
||||||
if (String.Equals(nodeString, patternString, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to number and compare
|
|
||||||
var nodeNumber = nodeEvaluationResult.ConvertToNumber();
|
|
||||||
if (!Double.IsNaN(nodeNumber) && nodeNumber >= 0d && nodeNumber <= (Double)Int32.MaxValue)
|
|
||||||
{
|
|
||||||
var patternNumber = patternEvaluationResult.ConvertToNumber();
|
|
||||||
if (!Double.IsNaN(patternNumber) && patternNumber >= 0 && patternNumber <= (Double)Int32.MaxValue)
|
|
||||||
{
|
|
||||||
nodeNumber = Math.Floor(nodeNumber);
|
|
||||||
patternNumber = Math.Floor(patternNumber);
|
|
||||||
if (nodeNumber == patternNumber)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not matched
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function is used to convert a pattern or a user-provided-expression into a
|
|
||||||
// consistent structure for easy comparison. The result is a stack containing only
|
|
||||||
// nodes of type NamedValue, Literal, or Wildcard. All Index nodes are discarded.
|
|
||||||
//
|
|
||||||
// For example, consider the pattern "needs.*.outputs". The expression tree looks like:
|
|
||||||
// Index(
|
|
||||||
// Index(
|
|
||||||
// NamedValue:needs,
|
|
||||||
// Wildcard:*
|
|
||||||
// ),
|
|
||||||
// Literal:"outputs"
|
|
||||||
// )
|
|
||||||
// The result would be:
|
|
||||||
// [
|
|
||||||
// NamedValue:needs
|
|
||||||
// Wildcard:*
|
|
||||||
// Literal:"outputs"
|
|
||||||
// ]
|
|
||||||
//
|
|
||||||
// Any nested expression trees that require further analysis, are returned separately.
|
|
||||||
// For example, consider the expression "needs.build.outputs[github.event.base_ref]"
|
|
||||||
// The result would be:
|
|
||||||
// [
|
|
||||||
// NamedValue:needs
|
|
||||||
// Literal:"build"
|
|
||||||
// Literal:"outputs"
|
|
||||||
// ]
|
|
||||||
// And the nested expression tree "github.event.base_ref" would be tracked as needing
|
|
||||||
// further analysis.
|
|
||||||
private static Stack<IExpressionNode> GetMatchSegments(
|
|
||||||
IExpressionNode node,
|
|
||||||
out List<ExpressionNode> needsFurtherAnalysis)
|
|
||||||
{
|
|
||||||
var result = new Stack<IExpressionNode>();
|
|
||||||
needsFurtherAnalysis = new List<ExpressionNode>();
|
|
||||||
|
|
||||||
// Node is a named-value
|
|
||||||
if (node is NamedValue)
|
|
||||||
{
|
|
||||||
result.Push(node);
|
|
||||||
}
|
|
||||||
// Node is an index
|
|
||||||
else if (node is Index index)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
//
|
|
||||||
// Parameter 1
|
|
||||||
//
|
|
||||||
var parameter1 = index.Parameters[1];
|
|
||||||
|
|
||||||
// Treat anything other than literal as a wildcard
|
|
||||||
result.Push(parameter1 is Literal ? parameter1 : new Wildcard());
|
|
||||||
|
|
||||||
// Further analysis required by the caller if parameter 1 is a Function/Operator/NamedValue
|
|
||||||
if (parameter1 is Container || parameter1 is NamedValue)
|
|
||||||
{
|
|
||||||
needsFurtherAnalysis.Add(parameter1);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Parameter 0
|
|
||||||
//
|
|
||||||
var parameter0 = index.Parameters[0];
|
|
||||||
|
|
||||||
// Parameter 0 is a named-value
|
|
||||||
if (parameter0 is NamedValue)
|
|
||||||
{
|
|
||||||
result.Push(parameter0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Parameter 0 is an index
|
|
||||||
else if (parameter0 is Index index2)
|
|
||||||
{
|
|
||||||
index = index2;
|
|
||||||
}
|
|
||||||
// Otherwise clear
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.Clear();
|
|
||||||
|
|
||||||
// Further analysis required by the caller if parameter 0 is a Function/Operator
|
|
||||||
if (parameter0 is Container)
|
|
||||||
{
|
|
||||||
needsFurtherAnalysis.Add(parameter0);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public interface IFunctionInfo
|
|
||||||
{
|
|
||||||
String Name { get; }
|
|
||||||
Int32 MinParameters { get; }
|
|
||||||
Int32 MaxParameters { get; }
|
|
||||||
Function CreateNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using System;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public interface INamedValueInfo
|
|
||||||
{
|
|
||||||
String Name { get; }
|
|
||||||
NamedValue CreateNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Used to mask secrets from trace messages and exception messages
|
|
||||||
/// </summary>
|
|
||||||
public interface ISecretMasker
|
|
||||||
{
|
|
||||||
String MaskSecrets(String input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public interface ITraceWriter
|
|
||||||
{
|
|
||||||
void Info(String message);
|
|
||||||
void Verbose(String message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
using GitHub.Actions.Expressions.Sdk;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public class NamedValueInfo<T> : INamedValueInfo
|
|
||||||
where T : NamedValue, new()
|
|
||||||
{
|
|
||||||
public NamedValueInfo(String name)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String Name { get; }
|
|
||||||
|
|
||||||
public NamedValue CreateNode()
|
|
||||||
{
|
|
||||||
return new T();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
internal sealed class NoOpSecretMasker : ISecretMasker
|
|
||||||
{
|
|
||||||
public String MaskSecrets(String input)
|
|
||||||
{
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +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.Tokens;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
public sealed class ParseException : ExpressionException
|
|
||||||
{
|
|
||||||
internal ParseException(ParseExceptionKind kind, Token token, String expression)
|
|
||||||
: base(secretMasker: null, message: String.Empty)
|
|
||||||
{
|
|
||||||
Expression = expression;
|
|
||||||
Kind = kind;
|
|
||||||
RawToken = token?.RawValue;
|
|
||||||
TokenIndex = token?.Index ?? 0;
|
|
||||||
String description;
|
|
||||||
switch (kind)
|
|
||||||
{
|
|
||||||
case ParseExceptionKind.ExceededMaxDepth:
|
|
||||||
description = $"Exceeded max expression depth {ExpressionConstants.MaxDepth}";
|
|
||||||
break;
|
|
||||||
case ParseExceptionKind.ExceededMaxLength:
|
|
||||||
description = $"Exceeded max expression length {ExpressionConstants.MaxLength}";
|
|
||||||
break;
|
|
||||||
case ParseExceptionKind.TooFewParameters:
|
|
||||||
description = "Too few parameters supplied";
|
|
||||||
break;
|
|
||||||
case ParseExceptionKind.TooManyParameters:
|
|
||||||
description = "Too many parameters supplied";
|
|
||||||
break;
|
|
||||||
case ParseExceptionKind.UnexpectedEndOfExpression:
|
|
||||||
description = "Unexpected end of expression";
|
|
||||||
break;
|
|
||||||
case ParseExceptionKind.UnexpectedSymbol:
|
|
||||||
description = "Unexpected symbol";
|
|
||||||
break;
|
|
||||||
case ParseExceptionKind.UnrecognizedFunction:
|
|
||||||
description = "Unrecognized function";
|
|
||||||
break;
|
|
||||||
case ParseExceptionKind.UnrecognizedNamedValue:
|
|
||||||
description = "Unrecognized named-value";
|
|
||||||
break;
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new Exception($"Unexpected parse exception kind '{kind}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token == null)
|
|
||||||
{
|
|
||||||
Message = description;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Message = $"{description}: '{RawToken}'. Located at position {TokenIndex + 1} within expression: {Expression}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal String Expression { get; }
|
|
||||||
|
|
||||||
internal ParseExceptionKind Kind { get; }
|
|
||||||
|
|
||||||
internal String RawToken { get; }
|
|
||||||
|
|
||||||
internal Int32 TokenIndex { get; }
|
|
||||||
|
|
||||||
public sealed override String Message { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace GitHub.Actions.Expressions
|
|
||||||
{
|
|
||||||
internal enum ParseExceptionKind
|
|
||||||
{
|
|
||||||
ExceededMaxDepth,
|
|
||||||
ExceededMaxLength,
|
|
||||||
TooFewParameters,
|
|
||||||
TooManyParameters,
|
|
||||||
UnexpectedEndOfExpression,
|
|
||||||
UnexpectedSymbol,
|
|
||||||
UnrecognizedFunction,
|
|
||||||
UnrecognizedNamedValue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
// <auto-generated/>
|
|
||||||
// *** AUTOMATICALLY GENERATED BY GenResourceClass -- DO NOT EDIT!!! ***
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Resources;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions {
|
|
||||||
|
|
||||||
|
|
||||||
internal static class ExpressionResources
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
//********************************************************************************************
|
|
||||||
/// Creates the resource manager instance.
|
|
||||||
//********************************************************************************************
|
|
||||||
static ExpressionResources()
|
|
||||||
{
|
|
||||||
s_resMgr = new ResourceManager("GitHub.Actions.Expressions.ExpressionResources", typeof(ExpressionResources).GetTypeInfo().Assembly);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ResourceManager Manager
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return s_resMgr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//********************************************************************************************
|
|
||||||
/// Returns a localized string given a resource string name.
|
|
||||||
//********************************************************************************************
|
|
||||||
public static String Get(
|
|
||||||
String resourceName)
|
|
||||||
{
|
|
||||||
return s_resMgr.GetString(resourceName, CultureInfo.CurrentUICulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
//********************************************************************************************
|
|
||||||
/// Returns a localized integer given a resource string name.
|
|
||||||
//********************************************************************************************
|
|
||||||
public static int GetInt(
|
|
||||||
String resourceName)
|
|
||||||
{
|
|
||||||
return (int)s_resMgr.GetObject(resourceName, CultureInfo.CurrentUICulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
//********************************************************************************************
|
|
||||||
/// Returns a localized string given a resource string name.
|
|
||||||
//********************************************************************************************
|
|
||||||
public static bool GetBool(
|
|
||||||
String resourceName)
|
|
||||||
{
|
|
||||||
return (bool)s_resMgr.GetObject(resourceName, CultureInfo.CurrentUICulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//********************************************************************************************
|
|
||||||
/// A little helper function to alleviate some typing associated with loading resources and
|
|
||||||
/// formatting the strings. In DEBUG builds, it also asserts that the number of format
|
|
||||||
/// arguments and the length of args match.
|
|
||||||
//********************************************************************************************
|
|
||||||
private static String Format( // The formatted resource string.
|
|
||||||
String resourceName, // The name of the resource.
|
|
||||||
params Object[] args) // Arguments to format.
|
|
||||||
{
|
|
||||||
String resource = Get(resourceName);
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
// Check to make sure that the number of format string arguments matches the number of
|
|
||||||
// arguments passed in.
|
|
||||||
int formatArgCount = 0;
|
|
||||||
bool[] argSeen = new bool[100];
|
|
||||||
for (int i = 0; i < resource.Length; i++)
|
|
||||||
{
|
|
||||||
if (resource[i] == '{')
|
|
||||||
{
|
|
||||||
if (i + 1 < resource.Length &&
|
|
||||||
resource[i + 1] == '{')
|
|
||||||
{
|
|
||||||
i++; // Skip the escaped curly braces.
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Move past the curly brace and leading whitespace.
|
|
||||||
i++;
|
|
||||||
while (Char.IsWhiteSpace(resource[i]))
|
|
||||||
{
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the argument number.
|
|
||||||
int length = 0;
|
|
||||||
while (i + length < resource.Length && Char.IsDigit(resource[i + length]))
|
|
||||||
{
|
|
||||||
length++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record it if it hasn't already been seen.
|
|
||||||
int argNumber = int.Parse(resource.Substring(i, length), CultureInfo.InvariantCulture);
|
|
||||||
if (!argSeen[argNumber])
|
|
||||||
{
|
|
||||||
formatArgCount++; // Count it as a formatting argument.
|
|
||||||
argSeen[argNumber] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Assert(args != null || formatArgCount == 0,
|
|
||||||
String.Format(CultureInfo.InvariantCulture, "The number of format arguments is {0}, but the args parameter is null.", formatArgCount));
|
|
||||||
Debug.Assert(args == null || formatArgCount == args.Length,
|
|
||||||
String.Format(CultureInfo.InvariantCulture, "Coding error using resource \"{0}\": The number of format arguments {1} != number of args {2}",
|
|
||||||
resourceName, formatArgCount, args != null ? args.Length : 0));
|
|
||||||
#endif // DEBUG
|
|
||||||
|
|
||||||
|
|
||||||
if (args == null)
|
|
||||||
{
|
|
||||||
return resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are any DateTime structs in the arguments, we need to bracket them
|
|
||||||
// to make sure they are within the supported range of the current calendar.
|
|
||||||
for (int i = 0; i < args.Length; i++)
|
|
||||||
{
|
|
||||||
// DateTime is a struct, we cannot use the as operator and null check.
|
|
||||||
if (args[i] is DateTime)
|
|
||||||
{
|
|
||||||
DateTime dateTime = (DateTime)args[i];
|
|
||||||
|
|
||||||
// We need to fetch the calendar on each Format call since it may change.
|
|
||||||
// Since we don't have more than one DateTime for resource, do not
|
|
||||||
// bother to cache this for the duration of the for loop.
|
|
||||||
Calendar calendar = DateTimeFormatInfo.CurrentInfo.Calendar;
|
|
||||||
if (dateTime > calendar.MaxSupportedDateTime)
|
|
||||||
{
|
|
||||||
args[i] = calendar.MaxSupportedDateTime;
|
|
||||||
}
|
|
||||||
else if (dateTime < calendar.MinSupportedDateTime)
|
|
||||||
{
|
|
||||||
args[i] = calendar.MinSupportedDateTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.Format(CultureInfo.CurrentCulture, resource, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// According to the documentation for the ResourceManager class, it should be sufficient to
|
|
||||||
// create a single static instance. The following is an excerpt from the 1.1 documentation.
|
|
||||||
// Using the methods of ResourceManager, a caller can access the resources for a particular
|
|
||||||
// culture using the GetObject and GetString methods. By default, these methods return the
|
|
||||||
// resource for the culture determined by the current cultural settings of the thread that made
|
|
||||||
// the call.
|
|
||||||
private static ResourceManager s_resMgr;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The maximum allowed memory size was exceeded while evaluating the following expression: {0}
|
|
||||||
/// </summary>
|
|
||||||
public static String ExceededAllowedMemory(object arg0) { return Format("ExceededAllowedMemory", arg0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 0 is replaced with a number.
|
|
||||||
///
|
|
||||||
/// Exceeded max expression depth {0}
|
|
||||||
/// </summary>
|
|
||||||
public static String ExceededMaxExpressionDepth(object arg0) { return Format("ExceededMaxExpressionDepth", arg0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 0 is replaced with a number.
|
|
||||||
///
|
|
||||||
/// Exceeded max expression length {0}
|
|
||||||
/// </summary>
|
|
||||||
public static String ExceededMaxExpressionLength(object arg0) { return Format("ExceededMaxExpressionLength", arg0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Expected a property name to follow the dereference operator '.'
|
|
||||||
/// </summary>
|
|
||||||
public static String ExpectedPropertyName() { return Get("ExpectedPropertyName"); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Expected '(' to follow a function
|
|
||||||
/// </summary>
|
|
||||||
public static String ExpectedStartParameter() { return Get("ExpectedStartParameter"); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The following format string references more arguments than were supplied: {0}
|
|
||||||
/// </summary>
|
|
||||||
public static String InvalidFormatArgIndex(object arg0) { return Format("InvalidFormatArgIndex", arg0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The format specifiers '{0}' are not valid for objects of type '{1}'
|
|
||||||
/// </summary>
|
|
||||||
public static String InvalidFormatSpecifiers(object arg0, object arg1) { return Format("InvalidFormatSpecifiers", arg0, arg1); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The following format string is invalid: {0}
|
|
||||||
/// </summary>
|
|
||||||
public static String InvalidFormatString(object arg0) { return Format("InvalidFormatString", arg0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Key not found '{0}'
|
|
||||||
/// </summary>
|
|
||||||
public static String KeyNotFound(object arg0) { return Format("KeyNotFound", arg0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 0 is replaced with the error message
|
|
||||||
///
|
|
||||||
/// {0}.
|
|
||||||
/// </summary>
|
|
||||||
public static String ParseErrorWithFwlink(object arg0) { return Format("ParseErrorWithFwlink", arg0); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 0 is replaced with the parse error message
|
|
||||||
/// 1 is replaced with the token
|
|
||||||
/// 2 is replaced with the character position within the string
|
|
||||||
/// 3 is replaced with the full statement
|
|
||||||
///
|
|
||||||
/// {0}: '{1}'. Located at position {2} within expression: {3}.
|
|
||||||
/// </summary>
|
|
||||||
public static String ParseErrorWithTokenInfo(object arg0, object arg1, object arg2, object arg3) { return Format("ParseErrorWithTokenInfo", arg0, arg1, arg2, arg3); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 0 is replaced with the from-type.
|
|
||||||
/// 1 is replaced with the to-type.
|
|
||||||
/// 2 is replaced with the value.
|
|
||||||
///
|
|
||||||
/// Unable to convert from {0} to {1}. Value: {2}
|
|
||||||
/// </summary>
|
|
||||||
public static String TypeCastError(object arg0, object arg1, object arg2) { return Format("TypeCastError", arg0, arg1, arg2); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 0 is replaced with the from-type.
|
|
||||||
/// 1 is replaced with the to-type.
|
|
||||||
///
|
|
||||||
/// Unable to convert from {0} to {1}.
|
|
||||||
/// </summary>
|
|
||||||
public static String TypeCastErrorNoValue(object arg0, object arg1) { return Format("TypeCastErrorNoValue", arg0, arg1); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 0 is replaced with the from-type.
|
|
||||||
/// 1 is replaced with the to-type.
|
|
||||||
/// 2 is replaced with the value.
|
|
||||||
/// 3 is replaced with the error message.
|
|
||||||
///
|
|
||||||
/// Unable to convert from {0} to {1}. Value: {2}. Error: {3}
|
|
||||||
/// </summary>
|
|
||||||
public static String TypeCastErrorWithError(object arg0, object arg1, object arg2, object arg3) { return Format("TypeCastErrorWithError", arg0, arg1, arg2, arg3); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unclosed function
|
|
||||||
/// </summary>
|
|
||||||
public static String UnclosedFunction() { return Get("UnclosedFunction"); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unclosed indexer
|
|
||||||
/// </summary>
|
|
||||||
public static String UnclosedIndexer() { return Get("UnclosedIndexer"); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unexpected symbol
|
|
||||||
/// </summary>
|
|
||||||
public static String UnexpectedSymbol() { return Get("UnexpectedSymbol"); }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unrecognized value
|
|
||||||
/// </summary>
|
|
||||||
public static String UnrecognizedValue() { return Get("UnrecognizedValue"); }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<data name="ExceededAllowedMemory" xml:space="preserve">
|
|
||||||
<value>The maximum allowed memory size was exceeded while evaluating the following expression: {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="ExceededMaxExpressionDepth" xml:space="preserve">
|
|
||||||
<value>Exceeded max expression depth {0}</value>
|
|
||||||
<comment>0 is replaced with a number.</comment>
|
|
||||||
</data>
|
|
||||||
<data name="ExceededMaxExpressionLength" xml:space="preserve">
|
|
||||||
<value>Exceeded max expression length {0}</value>
|
|
||||||
<comment>0 is replaced with a number.</comment>
|
|
||||||
</data>
|
|
||||||
<data name="ExpectedPropertyName" xml:space="preserve">
|
|
||||||
<value>Expected a property name to follow the dereference operator '.'</value>
|
|
||||||
</data>
|
|
||||||
<data name="ExpectedStartParameter" xml:space="preserve">
|
|
||||||
<value>Expected '(' to follow a function</value>
|
|
||||||
</data>
|
|
||||||
<data name="InvalidFormatArgIndex" xml:space="preserve">
|
|
||||||
<value>The following format string references more arguments than were supplied: {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="InvalidFormatSpecifiers" xml:space="preserve">
|
|
||||||
<value>The format specifiers '{0}' are not valid for objects of type '{1}'</value>
|
|
||||||
</data>
|
|
||||||
<data name="InvalidFormatString" xml:space="preserve">
|
|
||||||
<value>The following format string is invalid: {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="KeyNotFound" xml:space="preserve">
|
|
||||||
<value>Key not found '{0}'</value>
|
|
||||||
</data>
|
|
||||||
<data name="ParseErrorWithFwlink" xml:space="preserve">
|
|
||||||
<value>{0}.</value>
|
|
||||||
<comment>0 is replaced with the error message</comment>
|
|
||||||
</data>
|
|
||||||
<data name="ParseErrorWithTokenInfo" xml:space="preserve">
|
|
||||||
<value>{0}: '{1}'. Located at position {2} within expression: {3}.</value>
|
|
||||||
<comment>0 is replaced with the parse error message
|
|
||||||
1 is replaced with the token
|
|
||||||
2 is replaced with the character position within the string
|
|
||||||
3 is replaced with the full statement</comment>
|
|
||||||
</data>
|
|
||||||
<data name="TypeCastError" xml:space="preserve">
|
|
||||||
<value>Unable to convert from {0} to {1}. Value: {2}</value>
|
|
||||||
<comment>0 is replaced with the from-type.
|
|
||||||
1 is replaced with the to-type.
|
|
||||||
2 is replaced with the value.</comment>
|
|
||||||
</data>
|
|
||||||
<data name="TypeCastErrorNoValue" xml:space="preserve">
|
|
||||||
<value>Unable to convert from {0} to {1}.</value>
|
|
||||||
<comment>0 is replaced with the from-type.
|
|
||||||
1 is replaced with the to-type.</comment>
|
|
||||||
</data>
|
|
||||||
<data name="TypeCastErrorWithError" xml:space="preserve">
|
|
||||||
<value>Unable to convert from {0} to {1}. Value: {2}. Error: {3}</value>
|
|
||||||
<comment>0 is replaced with the from-type.
|
|
||||||
1 is replaced with the to-type.
|
|
||||||
2 is replaced with the value.
|
|
||||||
3 is replaced with the error message.</comment>
|
|
||||||
</data>
|
|
||||||
<data name="UnclosedFunction" xml:space="preserve">
|
|
||||||
<value>Unclosed function</value>
|
|
||||||
</data>
|
|
||||||
<data name="UnclosedIndexer" xml:space="preserve">
|
|
||||||
<value>Unclosed indexer</value>
|
|
||||||
</data>
|
|
||||||
<data name="UnexpectedSymbol" xml:space="preserve">
|
|
||||||
<value>Unexpected symbol</value>
|
|
||||||
</data>
|
|
||||||
<data name="UnrecognizedValue" xml:space="preserve">
|
|
||||||
<value>Unrecognized value</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk
|
|
||||||
{
|
|
||||||
public abstract class Container : ExpressionNode
|
|
||||||
{
|
|
||||||
public IReadOnlyList<ExpressionNode> Parameters => m_parameters.AsReadOnly();
|
|
||||||
|
|
||||||
public void AddParameter(ExpressionNode node)
|
|
||||||
{
|
|
||||||
m_parameters.Add(node);
|
|
||||||
node.Container = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly List<ExpressionNode> m_parameters = new List<ExpressionNode>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +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 System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk
|
|
||||||
{
|
|
||||||
public sealed class EvaluationContext
|
|
||||||
{
|
|
||||||
internal EvaluationContext(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options,
|
|
||||||
ExpressionNode node)
|
|
||||||
{
|
|
||||||
Trace = trace ?? throw new ArgumentNullException(nameof(trace));
|
|
||||||
SecretMasker = secretMasker ?? throw new ArgumentNullException(nameof(secretMasker));
|
|
||||||
State = state;
|
|
||||||
|
|
||||||
// Copy the options
|
|
||||||
options = new EvaluationOptions(copy: options);
|
|
||||||
if (options.MaxMemory == 0)
|
|
||||||
{
|
|
||||||
// Set a reasonable default max memory
|
|
||||||
options.MaxMemory = 1048576; // 1 mb
|
|
||||||
}
|
|
||||||
if (options.MaxCacheMemory <= 0)
|
|
||||||
{
|
|
||||||
// Set a reasonable default max cache bytes
|
|
||||||
options.MaxCacheMemory = 1048576; // 1 mb
|
|
||||||
}
|
|
||||||
Options = options;
|
|
||||||
Memory = new EvaluationMemory(options.MaxMemory, node);
|
|
||||||
|
|
||||||
m_traceResults = new Dictionary<ExpressionNode, String>();
|
|
||||||
m_traceMemory = new MemoryCounter(null, options.MaxCacheMemory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITraceWriter Trace { get; }
|
|
||||||
|
|
||||||
public ISecretMasker SecretMasker { get; }
|
|
||||||
|
|
||||||
public Object State { get; }
|
|
||||||
|
|
||||||
internal EvaluationMemory Memory { get; }
|
|
||||||
|
|
||||||
internal EvaluationOptions Options { get; }
|
|
||||||
|
|
||||||
internal void SetTraceResult(
|
|
||||||
ExpressionNode node,
|
|
||||||
EvaluationResult result)
|
|
||||||
{
|
|
||||||
// Remove if previously added. This typically should not happen. This could happen
|
|
||||||
// due to a badly authored function. So we'll handle it and track memory correctly.
|
|
||||||
if (m_traceResults.TryGetValue(node, out String oldValue))
|
|
||||||
{
|
|
||||||
m_traceMemory.Remove(oldValue);
|
|
||||||
m_traceResults.Remove(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check max memory
|
|
||||||
String value = ExpressionUtility.FormatValue(SecretMasker, result);
|
|
||||||
if (m_traceMemory.TryAdd(value))
|
|
||||||
{
|
|
||||||
// Store the result
|
|
||||||
m_traceResults[node] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal Boolean TryGetTraceResult(ExpressionNode node, out String value)
|
|
||||||
{
|
|
||||||
return m_traceResults.TryGetValue(node, out value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<ExpressionNode, String> m_traceResults = new Dictionary<ExpressionNode, String>();
|
|
||||||
private readonly MemoryCounter m_traceMemory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This is an internal class only.
|
|
||||||
///
|
|
||||||
/// This class is used to track current memory consumption
|
|
||||||
/// across the entire expression evaluation.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class EvaluationMemory
|
|
||||||
{
|
|
||||||
internal EvaluationMemory(
|
|
||||||
Int32 maxBytes,
|
|
||||||
ExpressionNode node)
|
|
||||||
{
|
|
||||||
m_maxAmount = maxBytes;
|
|
||||||
m_node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddAmount(
|
|
||||||
Int32 depth,
|
|
||||||
Int32 bytes,
|
|
||||||
Boolean trimDepth = false)
|
|
||||||
{
|
|
||||||
// Trim deeper depths
|
|
||||||
if (trimDepth)
|
|
||||||
{
|
|
||||||
while (m_maxActiveDepth > depth)
|
|
||||||
{
|
|
||||||
var amount = m_depths[m_maxActiveDepth];
|
|
||||||
|
|
||||||
if (amount > 0)
|
|
||||||
{
|
|
||||||
// Sanity check
|
|
||||||
if (amount > m_totalAmount)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Bytes to subtract exceeds total bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract from the total
|
|
||||||
checked
|
|
||||||
{
|
|
||||||
m_totalAmount -= amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the amount
|
|
||||||
m_depths[m_maxActiveDepth] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_maxActiveDepth--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grow the depths
|
|
||||||
if (depth > m_maxActiveDepth)
|
|
||||||
{
|
|
||||||
// Grow the list
|
|
||||||
while (m_depths.Count <= depth)
|
|
||||||
{
|
|
||||||
m_depths.Add(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust the max active depth
|
|
||||||
m_maxActiveDepth = depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
checked
|
|
||||||
{
|
|
||||||
// Add to the depth
|
|
||||||
m_depths[depth] += bytes;
|
|
||||||
|
|
||||||
// Add to the total
|
|
||||||
m_totalAmount += bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check max
|
|
||||||
if (m_totalAmount > m_maxAmount)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(ExpressionResources.ExceededAllowedMemory(m_node?.ConvertToExpression()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Int32 CalculateBytes(Object obj)
|
|
||||||
{
|
|
||||||
if (obj is String str)
|
|
||||||
{
|
|
||||||
// This measurement doesn't have to be perfect
|
|
||||||
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
|
|
||||||
|
|
||||||
checked
|
|
||||||
{
|
|
||||||
return c_stringBaseOverhead + ((str?.Length ?? 0) * sizeof(Char));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return c_minObjectSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const Int32 c_minObjectSize = 24;
|
|
||||||
private const Int32 c_stringBaseOverhead = 26;
|
|
||||||
private readonly List<Int32> m_depths = new List<Int32>();
|
|
||||||
private readonly Int32 m_maxAmount;
|
|
||||||
private readonly ExpressionNode m_node;
|
|
||||||
private Int32 m_maxActiveDepth = -1;
|
|
||||||
private Int32 m_totalAmount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk
|
|
||||||
{
|
|
||||||
internal sealed class EvaluationTraceWriter : ITraceWriter
|
|
||||||
{
|
|
||||||
public EvaluationTraceWriter(ITraceWriter trace, ISecretMasker secretMasker)
|
|
||||||
{
|
|
||||||
m_trace = trace;
|
|
||||||
m_secretMasker = secretMasker ?? throw new ArgumentNullException(nameof(secretMasker));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Info(String message)
|
|
||||||
{
|
|
||||||
if (m_trace != null)
|
|
||||||
{
|
|
||||||
message = m_secretMasker.MaskSecrets(message);
|
|
||||||
m_trace.Info(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Verbose(String message)
|
|
||||||
{
|
|
||||||
if (m_trace != null)
|
|
||||||
{
|
|
||||||
message = m_secretMasker.MaskSecrets(message);
|
|
||||||
m_trace.Verbose(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly ISecretMasker m_secretMasker;
|
|
||||||
private readonly ITraceWriter m_trace;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,187 +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;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk
|
|
||||||
{
|
|
||||||
public abstract class ExpressionNode : IExpressionNode
|
|
||||||
{
|
|
||||||
internal Container Container { get; set; }
|
|
||||||
|
|
||||||
internal Int32 Level { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name is used for tracing. Normally the parser will set the name. However if a node
|
|
||||||
/// is added manually, then the name may not be set and will fallback to the type name.
|
|
||||||
/// </summary>
|
|
||||||
public String Name
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return !String.IsNullOrEmpty(m_name) ? m_name : this.GetType().Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
m_name = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates whether the evalation result should be stored on the context and used
|
|
||||||
/// when the expanded result is traced.
|
|
||||||
/// </summary>
|
|
||||||
protected abstract Boolean TraceFullyExpanded { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IExpressionNode entry point.
|
|
||||||
/// </summary>
|
|
||||||
EvaluationResult IExpressionNode.Evaluate(
|
|
||||||
ITraceWriter trace,
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object state,
|
|
||||||
EvaluationOptions options)
|
|
||||||
{
|
|
||||||
if (Container != null)
|
|
||||||
{
|
|
||||||
// Do not localize. This is an SDK consumer error.
|
|
||||||
throw new NotSupportedException($"Expected {nameof(IExpressionNode)}.{nameof(Evaluate)} to be called on root node only.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var originalSecretMasker = secretMasker;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Evaluate
|
|
||||||
secretMasker = secretMasker ?? new NoOpSecretMasker();
|
|
||||||
trace = new EvaluationTraceWriter(trace, secretMasker);
|
|
||||||
var context = new EvaluationContext(trace, secretMasker, state, options, this);
|
|
||||||
var originalExpression = ConvertToExpression();
|
|
||||||
trace.Info($"Evaluating: {originalExpression}");
|
|
||||||
var result = Evaluate(context);
|
|
||||||
|
|
||||||
// Trace the result
|
|
||||||
TraceTreeResult(context, originalExpression, result.Value, result.Kind);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (secretMasker != null && secretMasker != originalSecretMasker)
|
|
||||||
{
|
|
||||||
(secretMasker as IDisposable)?.Dispose();
|
|
||||||
secretMasker = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This function is intended only for ExpressionNode authors to call. The EvaluationContext
|
|
||||||
/// caches result-state specific to the evaluation instance.
|
|
||||||
/// </summary>
|
|
||||||
public EvaluationResult Evaluate(EvaluationContext context)
|
|
||||||
{
|
|
||||||
// Evaluate
|
|
||||||
Level = Container == null ? 0 : Container.Level + 1;
|
|
||||||
TraceVerbose(context, Level, $"Evaluating {Name}:");
|
|
||||||
var coreResult = EvaluateCore(context, out ResultMemory coreMemory);
|
|
||||||
|
|
||||||
if (coreMemory == null)
|
|
||||||
{
|
|
||||||
coreMemory = new ResultMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to canonical value
|
|
||||||
var val = ExpressionUtility.ConvertToCanonicalValue(coreResult, out ValueKind kind, out Object raw);
|
|
||||||
|
|
||||||
// The depth can be safely trimmed when the total size of the core result is known,
|
|
||||||
// or when the total size of the core result can easily be determined.
|
|
||||||
var trimDepth = coreMemory.IsTotal || (Object.ReferenceEquals(raw, null) && ExpressionUtility.IsPrimitive(kind));
|
|
||||||
|
|
||||||
// Account for the memory overhead of the core result
|
|
||||||
var coreBytes = coreMemory.Bytes ?? EvaluationMemory.CalculateBytes(raw ?? val);
|
|
||||||
context.Memory.AddAmount(Level, coreBytes, trimDepth);
|
|
||||||
|
|
||||||
// Account for the memory overhead of the conversion result
|
|
||||||
if (!Object.ReferenceEquals(raw, null))
|
|
||||||
{
|
|
||||||
var conversionBytes = EvaluationMemory.CalculateBytes(val);
|
|
||||||
context.Memory.AddAmount(Level, conversionBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new EvaluationResult(context, Level, val, kind, raw);
|
|
||||||
|
|
||||||
// Store the trace result
|
|
||||||
if (this.TraceFullyExpanded)
|
|
||||||
{
|
|
||||||
context.SetTraceResult(this, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract String ConvertToExpression();
|
|
||||||
|
|
||||||
internal abstract String ConvertToExpandedExpression(EvaluationContext context);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the node
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The current expression context</param>
|
|
||||||
/// <param name="resultMemory">
|
|
||||||
/// Helps determine how much memory is being consumed across the evaluation of the expression.
|
|
||||||
/// </param>
|
|
||||||
protected abstract Object EvaluateCore(
|
|
||||||
EvaluationContext context,
|
|
||||||
out ResultMemory resultMemory);
|
|
||||||
|
|
||||||
protected MemoryCounter CreateMemoryCounter(EvaluationContext context)
|
|
||||||
{
|
|
||||||
return new MemoryCounter(this, context.Options.MaxMemory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TraceTreeResult(
|
|
||||||
EvaluationContext context,
|
|
||||||
String originalExpression,
|
|
||||||
Object result,
|
|
||||||
ValueKind kind)
|
|
||||||
{
|
|
||||||
// Get the expanded expression
|
|
||||||
String expandedExpression = ConvertToExpandedExpression(context);
|
|
||||||
|
|
||||||
// Format the result
|
|
||||||
String traceValue = ExpressionUtility.FormatValue(context.SecretMasker, result, kind);
|
|
||||||
|
|
||||||
// Only trace the expanded expression if it is meaningfully different (or if always showing)
|
|
||||||
if (context.Options.AlwaysTraceExpanded ||
|
|
||||||
(!String.Equals(expandedExpression, originalExpression, StringComparison.Ordinal) &&
|
|
||||||
!String.Equals(expandedExpression, traceValue, StringComparison.Ordinal)))
|
|
||||||
{
|
|
||||||
if (!context.Options.AlwaysTraceExpanded &&
|
|
||||||
kind == ValueKind.Number &&
|
|
||||||
String.Equals(expandedExpression, $"'{traceValue}'", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
// Don't bother tracing the expanded expression when the result is a number and the
|
|
||||||
// expanded expresion is a precisely matching string.
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Trace.Info($"Expanded: {expandedExpression}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always trace the result
|
|
||||||
context.Trace.Info($"Result: {traceValue}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceVerbose(
|
|
||||||
EvaluationContext context,
|
|
||||||
Int32 level,
|
|
||||||
String message)
|
|
||||||
{
|
|
||||||
context.Trace.Verbose(String.Empty.PadLeft(level * 2, '.') + (message ?? String.Empty));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String m_name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,295 +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 System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk
|
|
||||||
{
|
|
||||||
public static class ExpressionUtility
|
|
||||||
{
|
|
||||||
internal static Object ConvertToCanonicalValue(
|
|
||||||
Object val,
|
|
||||||
out ValueKind kind,
|
|
||||||
out Object raw)
|
|
||||||
{
|
|
||||||
raw = null;
|
|
||||||
|
|
||||||
if (Object.ReferenceEquals(val, null))
|
|
||||||
{
|
|
||||||
kind = ValueKind.Null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
else if (val is Boolean)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Boolean;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (val is Double)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Number;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (val is String)
|
|
||||||
{
|
|
||||||
kind = ValueKind.String;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (val is INull n)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Null;
|
|
||||||
raw = val;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
else if (val is IBoolean boolean)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Boolean;
|
|
||||||
raw = val;
|
|
||||||
return boolean.GetBoolean();
|
|
||||||
}
|
|
||||||
else if (val is INumber number)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Number;
|
|
||||||
raw = val;
|
|
||||||
return number.GetNumber();
|
|
||||||
}
|
|
||||||
else if (val is IString str)
|
|
||||||
{
|
|
||||||
kind = ValueKind.String;
|
|
||||||
raw = val;
|
|
||||||
return str.GetString();
|
|
||||||
}
|
|
||||||
else if (val is IReadOnlyObject)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Object;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (val is IReadOnlyArray)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Array;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (!val.GetType().GetTypeInfo().IsClass)
|
|
||||||
{
|
|
||||||
if (val is Decimal || val is Byte || val is SByte || val is Int16 || val is UInt16 || val is Int32 || val is UInt32 || val is Int64 || val is UInt64 || val is Single)
|
|
||||||
{
|
|
||||||
kind = ValueKind.Number;
|
|
||||||
return Convert.ToDouble(val);
|
|
||||||
}
|
|
||||||
else if (val is Enum)
|
|
||||||
{
|
|
||||||
var strVal = String.Format(CultureInfo.InvariantCulture, "{0:G}", val);
|
|
||||||
if (Double.TryParse(strVal, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out Double doubleValue))
|
|
||||||
{
|
|
||||||
kind = ValueKind.Number;
|
|
||||||
return doubleValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
kind = ValueKind.String;
|
|
||||||
return strVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kind = ValueKind.Object;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a string into it's parse token representation. Useful when programmatically constructing an expression.
|
|
||||||
/// For example the string "hello world" returns 'hello world'. Note, null will return the null token; pass empty string
|
|
||||||
/// if you want the empty string token instead.
|
|
||||||
/// </summary>
|
|
||||||
public static String ConvertToParseToken(String str)
|
|
||||||
{
|
|
||||||
if (str == null)
|
|
||||||
{
|
|
||||||
return FormatValue(null, null, ValueKind.Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return FormatValue(null, str, ValueKind.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a string into it's parse token representation. Useful when programmatically constructing an expression.
|
|
||||||
/// </summary>
|
|
||||||
public static String ConvertToParseToken(Double d)
|
|
||||||
{
|
|
||||||
return FormatValue(null, d, ValueKind.Number);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a string into it's parse token representation. Useful when programmatically constructing an expression.
|
|
||||||
/// </summary>
|
|
||||||
public static String ConvertToParseToken(Boolean b)
|
|
||||||
{
|
|
||||||
return FormatValue(null, b, ValueKind.Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static String FormatValue(
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
EvaluationResult evaluationResult)
|
|
||||||
{
|
|
||||||
return FormatValue(secretMasker, evaluationResult.Value, evaluationResult.Kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static String FormatValue(
|
|
||||||
ISecretMasker secretMasker,
|
|
||||||
Object value,
|
|
||||||
ValueKind kind)
|
|
||||||
{
|
|
||||||
switch (kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Null:
|
|
||||||
return ExpressionConstants.Null;
|
|
||||||
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
return ((Boolean)value) ? ExpressionConstants.True : ExpressionConstants.False;
|
|
||||||
|
|
||||||
case ValueKind.Number:
|
|
||||||
var strNumber = ((Double)value).ToString(ExpressionConstants.NumberFormat, CultureInfo.InvariantCulture);
|
|
||||||
return secretMasker != null ? secretMasker.MaskSecrets(strNumber) : strNumber;
|
|
||||||
|
|
||||||
case ValueKind.String:
|
|
||||||
// Mask secrets before string-escaping.
|
|
||||||
var strValue = secretMasker != null ? secretMasker.MaskSecrets(value as String) : value as String;
|
|
||||||
return $"'{StringEscape(strValue)}'";
|
|
||||||
|
|
||||||
case ValueKind.Array:
|
|
||||||
case ValueKind.Object:
|
|
||||||
return kind.ToString();
|
|
||||||
|
|
||||||
default: // Should never reach here.
|
|
||||||
throw new NotSupportedException($"Unable to convert to expanded expression. Unexpected value kind: {kind}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsLegalKeyword(String str)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrEmpty(str))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var first = str[0];
|
|
||||||
if ((first >= 'a' && first <= 'z') ||
|
|
||||||
(first >= 'A' && first <= 'Z') ||
|
|
||||||
first == '_')
|
|
||||||
{
|
|
||||||
for (var i = 1; i < str.Length; i++)
|
|
||||||
{
|
|
||||||
var c = str[i];
|
|
||||||
if ((c >= 'a' && c <= 'z') ||
|
|
||||||
(c >= 'A' && c <= 'Z') ||
|
|
||||||
(c >= '0' && c <= '9') ||
|
|
||||||
c == '_' ||
|
|
||||||
c == '-')
|
|
||||||
{
|
|
||||||
// OK
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Boolean IsPrimitive(ValueKind kind)
|
|
||||||
{
|
|
||||||
switch (kind)
|
|
||||||
{
|
|
||||||
case ValueKind.Null:
|
|
||||||
case ValueKind.Boolean:
|
|
||||||
case ValueKind.Number:
|
|
||||||
case ValueKind.String:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The rules here attempt to follow Javascript rules for coercing a string into a number
|
|
||||||
/// for comparison. That is, the Number() function in Javascript.
|
|
||||||
/// </summary>
|
|
||||||
internal static Double ParseNumber(String str)
|
|
||||||
{
|
|
||||||
// Trim
|
|
||||||
str = str?.Trim() ?? String.Empty;
|
|
||||||
|
|
||||||
// Empty
|
|
||||||
if (String.IsNullOrEmpty(str))
|
|
||||||
{
|
|
||||||
return 0d;
|
|
||||||
}
|
|
||||||
// Try parse
|
|
||||||
else if (Double.TryParse(str, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var value))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
// Check for 0x[0-9a-fA-F]+
|
|
||||||
else if (str[0] == '0' &&
|
|
||||||
str.Length > 2 &&
|
|
||||||
str[1] == 'x' &&
|
|
||||||
str.Skip(2).All(x => (x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F')))
|
|
||||||
{
|
|
||||||
// Try parse
|
|
||||||
if (Int32.TryParse(str.Substring(2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var integer))
|
|
||||||
{
|
|
||||||
return (Double)integer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise exceeds range
|
|
||||||
}
|
|
||||||
// Check for 0o[0-9]+
|
|
||||||
else if (str[0] == '0' &&
|
|
||||||
str.Length > 2 &&
|
|
||||||
str[1] == 'o' &&
|
|
||||||
str.Skip(2).All(x => x >= '0' && x <= '7'))
|
|
||||||
{
|
|
||||||
// Try parse
|
|
||||||
var integer = default(Int32?);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
integer = Convert.ToInt32(str.Substring(2), 8);
|
|
||||||
}
|
|
||||||
// Otherwise exceeds range
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success
|
|
||||||
if (integer != null)
|
|
||||||
{
|
|
||||||
return (Double)integer.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Infinity
|
|
||||||
else if (String.Equals(str, ExpressionConstants.Infinity, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return Double.PositiveInfinity;
|
|
||||||
}
|
|
||||||
// -Infinity
|
|
||||||
else if (String.Equals(str, ExpressionConstants.NegativeInfinity, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return Double.NegativeInfinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise NaN
|
|
||||||
return Double.NaN;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String StringEscape(String value)
|
|
||||||
{
|
|
||||||
return String.IsNullOrEmpty(value) ? String.Empty : value.Replace("'", "''");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk
|
|
||||||
{
|
|
||||||
public abstract class Function : Container
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Generally this should not be overridden. True indicates the result of the node is traced as part of the
|
|
||||||
/// "expanded" trace information. Otherwise the node expression is printed, and parameters to the node may or
|
|
||||||
/// may not be fully expanded - depending on each respective parameter's trace-fully-expanded setting.
|
|
||||||
///
|
|
||||||
/// The purpose is so the end user can understand how their expression expanded at run time. For example, consider
|
|
||||||
/// the expression: eq(variables.publish, 'true'). The runtime-expanded expression may be: eq('true', 'true')
|
|
||||||
/// </summary>
|
|
||||||
protected override Boolean TraceFullyExpanded => true;
|
|
||||||
|
|
||||||
public sealed override String ConvertToExpression()
|
|
||||||
{
|
|
||||||
return String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"{0}({1})",
|
|
||||||
Name,
|
|
||||||
String.Join(", ", Parameters.Select(x => x.ConvertToExpression())));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed override String ConvertToExpandedExpression(EvaluationContext context)
|
|
||||||
{
|
|
||||||
// Check if the result was stored
|
|
||||||
if (context.TryGetTraceResult(this, out String result))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"{0}({1})",
|
|
||||||
Name,
|
|
||||||
String.Join(", ", Parameters.Select(x => x.ConvertToExpandedExpression(context))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +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;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk.Functions
|
|
||||||
{
|
|
||||||
internal sealed class Contains : Function
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyExpanded => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(
|
|
||||||
EvaluationContext context,
|
|
||||||
out ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var left = Parameters[0].Evaluate(context);
|
|
||||||
if (left.IsPrimitive)
|
|
||||||
{
|
|
||||||
var leftString = left.ConvertToString();
|
|
||||||
|
|
||||||
var right = Parameters[1].Evaluate(context);
|
|
||||||
if (right.IsPrimitive)
|
|
||||||
{
|
|
||||||
var rightString = right.ConvertToString();
|
|
||||||
return leftString.IndexOf(rightString, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (left.TryGetCollectionInterface(out var collection) &&
|
|
||||||
collection is IReadOnlyArray array &&
|
|
||||||
array.Count > 0)
|
|
||||||
{
|
|
||||||
var right = Parameters[1].Evaluate(context);
|
|
||||||
foreach (var item in array)
|
|
||||||
{
|
|
||||||
var itemResult = EvaluationResult.CreateIntermediateResult(context, item);
|
|
||||||
if (right.AbstractEqual(itemResult))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +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;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk.Functions
|
|
||||||
{
|
|
||||||
internal sealed class EndsWith : Function
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyExpanded => false;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(
|
|
||||||
EvaluationContext context,
|
|
||||||
out ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var left = Parameters[0].Evaluate(context);
|
|
||||||
if (left.IsPrimitive)
|
|
||||||
{
|
|
||||||
var leftString = left.ConvertToString();
|
|
||||||
|
|
||||||
var right = Parameters[1].Evaluate(context);
|
|
||||||
if (right.IsPrimitive)
|
|
||||||
{
|
|
||||||
var rightString = right.ConvertToString();
|
|
||||||
return leftString.EndsWith(rightString, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,299 +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 System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk.Functions
|
|
||||||
{
|
|
||||||
public sealed class Format : Function
|
|
||||||
{
|
|
||||||
protected sealed override Object EvaluateCore(
|
|
||||||
EvaluationContext context,
|
|
||||||
out ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var format = Parameters[0].Evaluate(context).ConvertToString();
|
|
||||||
var index = 0;
|
|
||||||
var result = new FormatResultBuilder(this, context, CreateMemoryCounter(context));
|
|
||||||
while (index < format.Length)
|
|
||||||
{
|
|
||||||
var lbrace = format.IndexOf('{', index);
|
|
||||||
var rbrace = format.IndexOf('}', index);
|
|
||||||
|
|
||||||
// Left brace
|
|
||||||
if (lbrace >= 0 && (rbrace < 0 || rbrace > lbrace))
|
|
||||||
{
|
|
||||||
// Escaped left brace
|
|
||||||
if (SafeCharAt(format, lbrace + 1) == '{')
|
|
||||||
{
|
|
||||||
result.Append(format.Substring(index, lbrace - index + 1));
|
|
||||||
index = lbrace + 2;
|
|
||||||
}
|
|
||||||
// Left brace, number, optional format specifiers, right brace
|
|
||||||
else if (rbrace > lbrace + 1 &&
|
|
||||||
ReadArgIndex(format, lbrace + 1, out Byte argIndex, out Int32 endArgIndex) &&
|
|
||||||
ReadFormatSpecifiers(format, endArgIndex + 1, out String formatSpecifiers, out rbrace))
|
|
||||||
{
|
|
||||||
// Check parameter count
|
|
||||||
if (argIndex > Parameters.Count - 2)
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatArgIndex(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the portion before the left brace
|
|
||||||
if (lbrace > index)
|
|
||||||
{
|
|
||||||
result.Append(format.Substring(index, lbrace - index));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the arg
|
|
||||||
result.Append(argIndex, formatSpecifiers);
|
|
||||||
index = rbrace + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatString(format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Right brace
|
|
||||||
else if (rbrace >= 0)
|
|
||||||
{
|
|
||||||
// Escaped right brace
|
|
||||||
if (SafeCharAt(format, rbrace + 1) == '}')
|
|
||||||
{
|
|
||||||
result.Append(format.Substring(index, rbrace - index + 1));
|
|
||||||
index = rbrace + 2;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatString(format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Last segment
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.Append(format.Substring(index));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean ReadArgIndex(
|
|
||||||
String str,
|
|
||||||
Int32 startIndex,
|
|
||||||
out Byte result,
|
|
||||||
out Int32 endIndex)
|
|
||||||
{
|
|
||||||
// Count the number of digits
|
|
||||||
var length = 0;
|
|
||||||
while (Char.IsDigit(SafeCharAt(str, startIndex + length)))
|
|
||||||
{
|
|
||||||
length++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate at least one digit
|
|
||||||
if (length < 1)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
endIndex = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the number
|
|
||||||
endIndex = startIndex + length - 1;
|
|
||||||
return Byte.TryParse(str.Substring(startIndex, length), NumberStyles.None, CultureInfo.InvariantCulture, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean ReadFormatSpecifiers(
|
|
||||||
String str,
|
|
||||||
Int32 startIndex,
|
|
||||||
out String result,
|
|
||||||
out Int32 rbrace)
|
|
||||||
{
|
|
||||||
// No format specifiers
|
|
||||||
var c = SafeCharAt(str, startIndex);
|
|
||||||
if (c == '}')
|
|
||||||
{
|
|
||||||
result = String.Empty;
|
|
||||||
rbrace = startIndex;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate starts with ":"
|
|
||||||
if (c != ':')
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
rbrace = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the specifiers
|
|
||||||
var specifiers = new StringBuilder();
|
|
||||||
var index = startIndex + 1;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
// Validate not the end of the string
|
|
||||||
if (index >= str.Length)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
rbrace = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
c = str[index];
|
|
||||||
|
|
||||||
// Not right-brace
|
|
||||||
if (c != '}')
|
|
||||||
{
|
|
||||||
specifiers.Append(c);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
// Escaped right-brace
|
|
||||||
else if (SafeCharAt(str, index + 1) == '}')
|
|
||||||
{
|
|
||||||
specifiers.Append('}');
|
|
||||||
index += 2;
|
|
||||||
}
|
|
||||||
// Closing right-brace
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = specifiers.ToString();
|
|
||||||
rbrace = index;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Char SafeCharAt(
|
|
||||||
String str,
|
|
||||||
Int32 index)
|
|
||||||
{
|
|
||||||
if (str.Length > index)
|
|
||||||
{
|
|
||||||
return str[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FormatResultBuilder
|
|
||||||
{
|
|
||||||
internal FormatResultBuilder(
|
|
||||||
Format node,
|
|
||||||
EvaluationContext context,
|
|
||||||
MemoryCounter counter)
|
|
||||||
{
|
|
||||||
m_node = node;
|
|
||||||
m_context = context;
|
|
||||||
m_counter = counter;
|
|
||||||
m_cache = new ArgValue[node.Parameters.Count - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the final string. This is when lazy segments are evaluated.
|
|
||||||
public override String ToString()
|
|
||||||
{
|
|
||||||
return String.Join(
|
|
||||||
String.Empty,
|
|
||||||
m_segments.Select(obj =>
|
|
||||||
{
|
|
||||||
if (obj is Lazy<String> lazy)
|
|
||||||
{
|
|
||||||
return lazy.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return obj as String;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append a static value
|
|
||||||
internal void Append(String value)
|
|
||||||
{
|
|
||||||
if (value?.Length > 0)
|
|
||||||
{
|
|
||||||
// Track memory
|
|
||||||
m_counter.Add(value);
|
|
||||||
|
|
||||||
// Append the segment
|
|
||||||
m_segments.Add(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append an argument
|
|
||||||
internal void Append(
|
|
||||||
Int32 argIndex,
|
|
||||||
String formatSpecifiers)
|
|
||||||
{
|
|
||||||
// Delay execution until the final ToString
|
|
||||||
m_segments.Add(new Lazy<String>(() =>
|
|
||||||
{
|
|
||||||
String result;
|
|
||||||
|
|
||||||
// Get the arg from the cache
|
|
||||||
var argValue = m_cache[argIndex];
|
|
||||||
|
|
||||||
// Evaluate the arg and cache the result
|
|
||||||
if (argValue == null)
|
|
||||||
{
|
|
||||||
// The evaluation result is required when format specifiers are used. Otherwise the string
|
|
||||||
// result is required. Go ahead and store both values. Since ConvertToString produces tracing,
|
|
||||||
// we need to run that now so the tracing appears in order in the log.
|
|
||||||
var evaluationResult = m_node.Parameters[argIndex + 1].Evaluate(m_context);
|
|
||||||
var stringResult = evaluationResult.ConvertToString();
|
|
||||||
argValue = new ArgValue(evaluationResult, stringResult);
|
|
||||||
m_cache[argIndex] = argValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No format specifiers
|
|
||||||
if (String.IsNullOrEmpty(formatSpecifiers))
|
|
||||||
{
|
|
||||||
result = argValue.StringResult;
|
|
||||||
}
|
|
||||||
// Invalid
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new FormatException(ExpressionResources.InvalidFormatSpecifiers(formatSpecifiers, argValue.EvaluationResult.Kind));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track memory
|
|
||||||
if (!String.IsNullOrEmpty(result))
|
|
||||||
{
|
|
||||||
m_counter.Add(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly ArgValue[] m_cache;
|
|
||||||
private readonly EvaluationContext m_context;
|
|
||||||
private readonly MemoryCounter m_counter;
|
|
||||||
private readonly Format m_node;
|
|
||||||
private readonly List<Object> m_segments = new List<Object>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stores an EvaluateResult and the value converted to a String.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class ArgValue
|
|
||||||
{
|
|
||||||
public ArgValue(
|
|
||||||
EvaluationResult evaluationResult,
|
|
||||||
String stringResult)
|
|
||||||
{
|
|
||||||
EvaluationResult = evaluationResult;
|
|
||||||
StringResult = stringResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EvaluationResult EvaluationResult { get; }
|
|
||||||
|
|
||||||
public String StringResult { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +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 System.IO;
|
|
||||||
using GitHub.Actions.Expressions.Data;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk.Functions
|
|
||||||
{
|
|
||||||
internal sealed class FromJson : Function
|
|
||||||
{
|
|
||||||
protected sealed override Object EvaluateCore(
|
|
||||||
EvaluationContext context,
|
|
||||||
out ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var json = Parameters[0].Evaluate(context).ConvertToString();
|
|
||||||
|
|
||||||
if (context.Options.StrictJsonParsing)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return JsonParser.Parse(json);
|
|
||||||
}
|
|
||||||
catch (System.Text.Json.JsonException ex)
|
|
||||||
{
|
|
||||||
throw new System.Text.Json.JsonException($"Error parsing fromJson: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new System.Text.Json.JsonException($"Unexpected error parsing fromJson: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var stringReader = new StringReader(json);
|
|
||||||
using var jsonReader = new JsonTextReader(stringReader) { DateParseHandling = DateParseHandling.None, FloatParseHandling = FloatParseHandling.Double };
|
|
||||||
var token = JToken.ReadFrom(jsonReader);
|
|
||||||
return token.ToExpressionData();
|
|
||||||
}
|
|
||||||
catch (JsonReaderException ex)
|
|
||||||
{
|
|
||||||
throw new JsonReaderException("Error parsing fromJson", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +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 System.Text;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk.Functions
|
|
||||||
{
|
|
||||||
internal sealed class Join : Function
|
|
||||||
{
|
|
||||||
protected sealed override Boolean TraceFullyExpanded => true;
|
|
||||||
|
|
||||||
protected sealed override Object EvaluateCore(
|
|
||||||
EvaluationContext context,
|
|
||||||
out ResultMemory resultMemory)
|
|
||||||
{
|
|
||||||
resultMemory = null;
|
|
||||||
var items = Parameters[0].Evaluate(context);
|
|
||||||
|
|
||||||
// Array
|
|
||||||
if (items.TryGetCollectionInterface(out var collection) &&
|
|
||||||
collection is IReadOnlyArray array &&
|
|
||||||
array.Count > 0)
|
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
|
||||||
var memory = new MemoryCounter(this, context.Options.MaxMemory);
|
|
||||||
|
|
||||||
// Append the first item
|
|
||||||
var item = array[0];
|
|
||||||
var itemResult = EvaluationResult.CreateIntermediateResult(context, item);
|
|
||||||
var itemString = itemResult.ConvertToString();
|
|
||||||
memory.Add(itemString);
|
|
||||||
result.Append(itemString);
|
|
||||||
|
|
||||||
// More items?
|
|
||||||
if (array.Count > 1)
|
|
||||||
{
|
|
||||||
var separator = ",";
|
|
||||||
if (Parameters.Count > 1)
|
|
||||||
{
|
|
||||||
var separatorResult = Parameters[1].Evaluate(context);
|
|
||||||
if (separatorResult.IsPrimitive)
|
|
||||||
{
|
|
||||||
separator = separatorResult.ConvertToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 1; i < array.Count; i++)
|
|
||||||
{
|
|
||||||
// Append the separator
|
|
||||||
memory.Add(separator);
|
|
||||||
result.Append(separator);
|
|
||||||
|
|
||||||
// Append the next item
|
|
||||||
var nextItem = array[i];
|
|
||||||
var nextItemResult = EvaluationResult.CreateIntermediateResult(context, nextItem);
|
|
||||||
var nextItemString = nextItemResult.ConvertToString();
|
|
||||||
memory.Add(nextItemString);
|
|
||||||
result.Append(nextItemString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.ToString();
|
|
||||||
}
|
|
||||||
// Primitive
|
|
||||||
else if (items.IsPrimitive)
|
|
||||||
{
|
|
||||||
return items.ConvertToString();
|
|
||||||
}
|
|
||||||
// Otherwise return empty string
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return String.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +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 System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using GitHub.Actions.Expressions.Data;
|
|
||||||
|
|
||||||
namespace GitHub.Actions.Expressions.Sdk.Functions
|
|
||||||
{
|
|
||||||
internal sealed class JsonParser
|
|
||||||
{
|
|
||||||
public static ExpressionData Parse(string json)
|
|
||||||
{
|
|
||||||
var reader = new Utf8JsonReader(
|
|
||||||
Encoding.UTF8.GetBytes(json),
|
|
||||||
new JsonReaderOptions{
|
|
||||||
AllowTrailingCommas = false,
|
|
||||||
CommentHandling = JsonCommentHandling.Disallow,
|
|
||||||
MaxDepth = 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
// EOF?
|
|
||||||
if (!reader.Read())
|
|
||||||
{
|
|
||||||
throw new Exception("Expected at least one JSON token");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read
|
|
||||||
var result = ReadRecursive(ref reader);
|
|
||||||
|
|
||||||
// Not EOF?
|
|
||||||
if (reader.Read())
|
|
||||||
{
|
|
||||||
throw new Exception($"Expected end of JSON but encountered '{reader.TokenType}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ExpressionData ReadRecursive(ref Utf8JsonReader reader)
|
|
||||||
{
|
|
||||||
switch (reader.TokenType)
|
|
||||||
{
|
|
||||||
case JsonTokenType.StartArray:
|
|
||||||
return ReadArray(ref reader);
|
|
||||||
case JsonTokenType.StartObject:
|
|
||||||
return ReadObject(ref reader);
|
|
||||||
case JsonTokenType.Null:
|
|
||||||
return null;
|
|
||||||
case JsonTokenType.False:
|
|
||||||
return new BooleanExpressionData(false);
|
|
||||||
case JsonTokenType.True:
|
|
||||||
return new BooleanExpressionData(true);
|
|
||||||
case JsonTokenType.Number:
|
|
||||||
return new NumberExpressionData(reader.GetDouble());
|
|
||||||
case JsonTokenType.String:
|
|
||||||
return new StringExpressionData(reader.GetString());
|
|
||||||
default:
|
|
||||||
throw new Exception($"Unexpected token type '{reader.TokenType}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ArrayExpressionData ReadArray(ref Utf8JsonReader reader)
|
|
||||||
{
|
|
||||||
var result = new ArrayExpressionData();
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
// End array
|
|
||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item
|
|
||||||
result.Add(ReadRecursive(ref reader));
|
|
||||||
}
|
|
||||||
|
|
||||||
// EOF
|
|
||||||
throw new Exception($"Unexpected end of JSON while reading array");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DictionaryExpressionData ReadObject(ref Utf8JsonReader reader)
|
|
||||||
{
|
|
||||||
var result = new DictionaryExpressionData();
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
var key = null as string;
|
|
||||||
switch (reader.TokenType)
|
|
||||||
{
|
|
||||||
// End object
|
|
||||||
case JsonTokenType.EndObject:
|
|
||||||
return result;
|
|
||||||
|
|
||||||
// Property name
|
|
||||||
case JsonTokenType.PropertyName:
|
|
||||||
key = reader.GetString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Exception($"Unexpected token type '{reader.TokenType}' while reading object");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value
|
|
||||||
var value = null as ExpressionData;
|
|
||||||
if (reader.Read())
|
|
||||||
{
|
|
||||||
value = ReadRecursive(ref reader);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Exception("Unexpected end of JSON when reading object-pair value");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add
|
|
||||||
if (!result.ContainsKey(key))
|
|
||||||
{
|
|
||||||
result.Add(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EOF
|
|
||||||
throw new Exception($"Unexpected end of JSON while reading object");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user