Compare commits

..

13 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
659915810c Add documentation index link to main README
Co-authored-by: salmanmkc <32169182+salmanmkc@users.noreply.github.com>
2025-08-14 10:34:53 +00:00
copilot-swe-agent[bot]
ea03aa2a6e Enhance contribution guide, automation docs, and add documentation index
Co-authored-by: salmanmkc <32169182+salmanmkc@users.noreply.github.com>
2025-08-14 10:33:31 +00:00
copilot-swe-agent[bot]
691793a922 Improve README and OS prerequisites documentation
Co-authored-by: salmanmkc <32169182+salmanmkc@users.noreply.github.com>
2025-08-14 10:30:04 +00:00
copilot-swe-agent[bot]
f11013518c Initial plan 2025-08-14 10:14:18 +00:00
Salman Chishti
0ebdf9e83d Prepare runner release v2.328.0 (#3984) 2025-08-13 17:38:32 +01:00
dependabot[bot]
6543bf206b Bump actions/checkout from 4 to 5 (#3982)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-13 12:44:39 +01:00
dependabot[bot]
a942627965 Bump actions/download-artifact from 4 to 5 (#3973)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 19:32:29 -04:00
dependabot[bot]
83539166c9 Bump Azure.Storage.Blobs from 12.24.0 to 12.25.0 (#3974)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 23:23:54 +00:00
dependabot[bot]
1c1e8bfd18 Bump Microsoft.NET.Test.Sdk from 17.13.0 to 17.14.1 (#3975)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 19:17:20 -04:00
Cory Calahan
59177fa379 Redirect supported OS doc section to current public Docs location (#3979) 2025-08-07 18:49:02 -04:00
djs-intel
2d7635a7f0 Update Node20 and Node24 to latest (#3972) 2025-08-07 22:41:18 +00:00
Salman Chishti
0203cf24d3 Node 20 -> Node 24 migration feature flagging, opt-in and opt-out environment variables (#3948) 2025-08-07 16:30:03 +00:00
Joshua Brooks
5e74a4d8e4 Add V2 flow for runner deletion (#3954) 2025-08-07 10:52:46 -04:00
27 changed files with 980 additions and 995 deletions

View File

@@ -50,7 +50,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Build runner layout
- name: Build & Layout Release

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ jobs:
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Make sure ./releaseVersion match ./src/runnerversion
# Query GitHub release ensure version is not used
@@ -86,7 +86,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Build runner layout
- name: Build & Layout Release
@@ -129,41 +129,41 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# Download runner package tar.gz/zip produced by 'build' job
- name: Download Artifact (win-x64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-win-x64
path: ./
- name: Download Artifact (win-arm64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-win-arm64
path: ./
- name: Download Artifact (osx-x64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-osx-x64
path: ./
- name: Download Artifact (osx-arm64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-osx-arm64
path: ./
- name: Download Artifact (linux-x64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-linux-x64
path: ./
- name: Download Artifact (linux-arm)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-linux-arm
path: ./
- name: Download Artifact (linux-arm64)
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: runner-packages-linux-arm64
path: ./
@@ -296,7 +296,7 @@ jobs:
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Compute image version
id: image

View File

@@ -12,28 +12,33 @@ The runner is the application that runs a job from a GitHub Actions workflow. It
For more information about installing and using self-hosted runners, see [Adding self-hosted runners](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/adding-self-hosted-runners) and [Using self-hosted runners in a workflow](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-self-hosted-runners-in-a-workflow)
Runner releases:
## Download and Install
![win](docs/res/win_sm.png) [Pre-reqs](docs/start/envwin.md) | [Download](https://github.com/actions/runner/releases)
![win](docs/res/win_sm.png) **Windows**: [Prerequisites](docs/start/envwin.md) | [Download](https://github.com/actions/runner/releases)
![macOS](docs/res/apple_sm.png) [Pre-reqs](docs/start/envosx.md) | [Download](https://github.com/actions/runner/releases)
![macOS](docs/res/apple_sm.png) **macOS**: [Prerequisites](docs/start/envosx.md) | [Download](https://github.com/actions/runner/releases)
![linux](docs/res/linux_sm.png) [Pre-reqs](docs/start/envlinux.md) | [Download](https://github.com/actions/runner/releases)
![linux](docs/res/linux_sm.png) **Linux**: [Prerequisites](docs/start/envlinux.md) | [Download](https://github.com/actions/runner/releases)
### Note
## Documentation and Resources
Thank you for your interest in this GitHub repo, however, right now we are not taking contributions.
- 📚 **[Complete Documentation Index](docs/README.md)** - Comprehensive guide to all documentation
- 📖 **[Contributing Guide](docs/contribute.md)** - Development setup, building, and testing
- 🔧 **[Automation Scripts](docs/automate.md)** - Automate runner setup and configuration
- 🛠️ **[Troubleshooting Guides](docs/checks/README.md)** - Common issues and solutions
- 🏗️ **[Architecture Decision Records](docs/adrs/README.md)** - Important architectural decisions
- ⚙️ **Platform Prerequisites:** [Linux](docs/start/envlinux.md) | [Windows](docs/start/envwin.md) | [macOS](docs/start/envosx.md)
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features were working on and what stage theyre in.
## Support and Community
We are taking the following steps to better direct requests related to GitHub Actions, including:
Thank you for your interest in this repository. Please note our current contribution and support guidelines:
1. We will be directing questions and support requests to our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions)
**Bug Reports:** You are welcome to report bugs in this repository through Issues.
2. High Priority bugs can be reported through Community Discussions or you can report these to our support team https://support.github.com/contact/bug-report.
**Feature Requests:** Please submit feature and enhancement requests on the [GitHub Community Discussions](https://github.com/orgs/community/discussions/categories/actions) page.
3. Security Issues should be handled as per our [security.md](security.md)
**Support Questions:** For help and support, please use our [Community Discussions area](https://github.com/orgs/community/discussions/categories/actions).
We will still provide security updates for this project and fix major breaking changes during this time.
**Security Issues:** Please report security vulnerabilities following our [security policy](security.md).
You are welcome to still raise bugs in this repo.
**High Priority Issues:** Critical bugs can be reported through Community Discussions or our [support team](https://support.github.com/contact/bug-report).

61
docs/README.md Normal file
View File

@@ -0,0 +1,61 @@
# GitHub Actions Runner Documentation
Welcome to the GitHub Actions Runner documentation. This guide will help you get started with self-hosted runners, contribute to the project, and troubleshoot common issues.
## 🚀 Getting Started
### Installation and Setup
- **[Linux Prerequisites](start/envlinux.md)** - Complete setup guide for Linux systems
- **[Windows Prerequisites](start/envwin.md)** - Complete setup guide for Windows systems
- **[macOS Prerequisites](start/envosx.md)** - Complete setup guide for macOS systems
### Quick Start
1. Download the [latest runner release](https://github.com/actions/runner/releases)
2. Follow the platform-specific prerequisites guide above
3. Configure your runner with `./config.sh` (Linux/macOS) or `.\config.cmd` (Windows)
4. Start the runner with `./run.sh` (Linux/macOS) or `.\run.cmd` (Windows)
## 🔧 Administration and Automation
- **[Automation Scripts](automate.md)** - Automate runner deployment and management
- **[Troubleshooting Guides](checks/)** - Common issues and solutions
## 🏗️ Development and Contributing
- **[Contributing Guide](contribute.md)** - Development setup, building, and testing
- **[Architecture Decision Records](adrs/README.md)** - Important architectural decisions and design patterns
## 📋 Reference Materials
### System Checks and Troubleshooting
- **[Network Connectivity](checks/network.md)** - Troubleshoot network issues
- **[Git Configuration](checks/git.md)** - Git-related problems
- **[Actions Troubleshooting](checks/actions.md)** - Action-specific issues
- **[SSL Certificate Issues](checks/sslcert.md)** - Certificate and TLS problems
- **[Node.js Issues](checks/nodejs.md)** - Node.js runtime problems
- **[Internet Connectivity](checks/internet.md)** - General connectivity issues
### Development Resources
- **[Visual Studio Code Setup](contribute/vscode.md)** - IDE configuration for development
- **[Design Documentation](design/)** - Technical design documents
## 🆘 Getting Help
### Community Support
- **[GitHub Community Discussions](https://github.com/orgs/community/discussions/categories/actions)** - Ask questions and get help from the community
- **[GitHub Support](https://support.github.com/contact/bug-report)** - Report critical bugs or get professional support
### Reporting Issues
- **Bug Reports**: Open an issue in this repository
- **Feature Requests**: Use [GitHub Community Discussions](https://github.com/orgs/community/discussions/categories/actions-and-packages)
- **Security Issues**: Follow our [security policy](../security.md)
## 📖 Additional Resources
- **[GitHub Actions Documentation](https://docs.github.com/en/actions)** - Official GitHub Actions documentation
- **[Self-hosted Runners Guide](https://docs.github.com/en/actions/hosting-your-own-runners)** - GitHub's official self-hosted runner documentation
- **[Runner Releases](https://github.com/actions/runner/releases)** - Download the latest runner versions
---
> **Note**: This documentation is maintained by the GitHub Actions team and the community. If you find any issues or have suggestions for improvement, please open an issue or contribute a pull request.

View File

@@ -76,3 +76,76 @@ Repo level one-liner. NOTE: replace with yourorg/yourrepo (repo level) or just
```bash
curl -s https://raw.githubusercontent.com/actions/runner/main/scripts/delete.sh | bash -s yourorg/yourrepo runnername
```
## Troubleshooting
### Common Issues
#### Permission Denied
```bash
# Ensure scripts have execute permissions
chmod +x ./config.sh ./run.sh
```
#### PAT Token Issues
```bash
# Verify your PAT has the correct scopes:
# - repo (for repository-level runners)
# - admin:org (for organization-level runners)
export RUNNER_CFG_PAT=your_token_here
echo $RUNNER_CFG_PAT # Verify it's set
```
#### Network Connectivity
```bash
# Test GitHub connectivity
curl -H "Authorization: token $RUNNER_CFG_PAT" https://api.github.com/user
# For GitHub Enterprise Server
curl -H "Authorization: token $RUNNER_CFG_PAT" https://your-github-enterprise/api/v3/user
```
#### Service Installation Fails
```bash
# Check if running as appropriate user
whoami
# For Linux - ensure systemd is available
systemctl --version
# For macOS - ensure launchd is available
launchctl version
```
#### Runner Registration Fails
```bash
# Check if runner already exists
curl -H "Authorization: token $RUNNER_CFG_PAT" \
"https://api.github.com/repos/OWNER/REPO/actions/runners"
# Remove existing runner if needed
./config.sh remove --token $RUNNER_CFG_PAT
```
### Getting Help
- **Configuration Issues**: Check the [Prerequisites](start/envlinux.md) for your platform
- **Network Problems**: Review [network troubleshooting guide](checks/network.md)
- **General Support**: Visit [GitHub Community Discussions](https://github.com/orgs/community/discussions/categories/actions)
### Advanced Examples
#### Organization-level Runner with Custom Labels
```bash
export RUNNER_CFG_PAT=your_org_pat
curl -s https://raw.githubusercontent.com/actions/runner/main/scripts/create-latest-svc.sh | \
bash -s -- -s myorg -n prod-runner-1 -l production,linux,docker
```
#### Repository-level Runner for GitHub Enterprise
```bash
export RUNNER_CFG_PAT=your_ghe_pat
curl -s https://raw.githubusercontent.com/actions/runner/main/scripts/create-latest-svc.sh | \
bash -s -- -s myorg/myrepo -g github.company.com -n build-server -u builder
```
```

66
docs/checks/README.md Normal file
View File

@@ -0,0 +1,66 @@
# Troubleshooting Guides
This directory contains troubleshooting guides for common issues you might encounter when setting up or running GitHub Actions self-hosted runners.
## Quick Reference
| Issue Type | Guide | Description |
|------------|-------|-------------|
| 🌐 **Network** | [network.md](network.md) | Connection issues, proxy, firewall problems |
| 🔒 **SSL/TLS** | [sslcert.md](sslcert.md) | Certificate and TLS handshake issues |
| 📦 **Git** | [git.md](git.md) | Git configuration and repository access |
| ⚡ **Actions** | [actions.md](actions.md) | Action-specific runtime issues |
| 🟢 **Node.js** | [nodejs.md](nodejs.md) | Node.js runtime and npm issues |
| 🌍 **Internet** | [internet.md](internet.md) | General internet connectivity |
## Common First Steps
Before diving into specific guides, try these general troubleshooting steps:
### 1. Check Basic Connectivity
```bash
# Test GitHub API access
curl -I https://api.github.com/
# For GitHub Enterprise Server
curl -I https://your-github-enterprise.com/api/v3/
```
### 2. Verify Runner Status
```bash
# Check if runner service is running
./svc.sh status
# View recent logs
tail -f _diag/Runner_*.log
```
### 3. Test Runner Configuration
```bash
# Re-run configuration
./config.sh
# Test connection without running
./run.sh --check
```
## Getting Additional Help
If these guides don't resolve your issue:
1. **Search existing issues** in the [runner repository](https://github.com/actions/runner/issues)
2. **Check GitHub Status** at [githubstatus.com](https://githubstatus.com)
3. **Ask the community** in [GitHub Community Discussions](https://github.com/orgs/community/discussions/categories/actions)
4. **Contact support** for critical issues via [GitHub Support](https://support.github.com/contact)
## Contributing
Found a solution to a common problem not covered here? Consider contributing:
1. Create a new `.md` file for the issue type
2. Follow the format of existing guides
3. Submit a pull request with your improvements
---
💡 **Tip**: Always check the `_diag/` directory for detailed log files when troubleshooting issues.

View File

@@ -1,5 +1,24 @@
# Contributions
## Table of Contents
- [Getting Started](#getting-started)
- [Issues](#issues)
- [Enhancements and Feature Requests](#enhancements-and-feature-requests)
- [Required Dev Dependencies](#required-dev-dependencies)
- [Quickstart: Run a Job from a Real Repository](#quickstart-run-a-job-from-a-real-repository)
- [Development Life Cycle](#development-life-cycle)
- [Clone Repository](#clone-repository)
- [Build Layout](#build-layout)
- [Test Layout](#test-layout)
- [Configure Runner](#configure-runner)
- [Run Runner](#run-runner)
- [View Logs](#view-logs)
- [Editors](#editors)
- [Styling](#styling)
## Getting Started
We welcome contributions in the form of issues and pull requests. We view the contributions and the process as the same for github and external contributors. Please note the runner typically requires changes across the entire system and we aim for issues in the runner to be entirely self contained and fixable here. Therefore, we will primarily handle bug issues opened in this repo and we kindly request you to create all feature and enhancement requests on the [GitHub Feedback](https://github.com/community/community/discussions/categories/actions-and-packages) page.
> IMPORTANT: Building your own runner is critical for the dev inner loop process when contributing changes. However, only runners built and distributed by GitHub (releases) are supported in production. Be aware that workflows and orchestrations run service side with the runner being a remote process to run steps. For that reason, the service can pull the runner forward so customizations can be lost.
@@ -124,8 +143,8 @@ cd runner/_layout
./config.(sh/cmd) # configure your custom runner
```
You will need your the name of your repository and a runner registration token.
Check [Quickstart](##Quickstart:-Run-a-job-from-a-real-repository) if you don't know how to get this token.
You will need the name of your repository and a runner registration token.
Check the [Quickstart section](#quickstart-run-a-job-from-a-real-repository) if you don't know how to get this token.
These can also be passed down as arguments to `config.(sh/cmd)`:
```bash

View File

@@ -4,12 +4,23 @@
## Supported Distributions and Versions
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)."
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/reference/runners/self-hosted-runners#linux)."
## Install .Net Core 3.x Linux Dependencies
## Quick Setup
The `./config.sh` script will automatically check and guide you through installing .NET dependencies:
```bash
./config.sh
# If dependencies are missing, run:
./bin/installdependencies.sh
```
## Install .NET Core Linux Dependencies
The `./config.sh` will check .NET Core dependencies during runner configuration.
You might see something like this which indicates a dependency is missing:
The `./config.sh` will check .Net Core 3.x dependencies during runner configuration.
You might see something like this which indicate a dependency's missing.
```bash
./config.sh
libunwind.so.8 => not found
@@ -17,34 +28,87 @@ You might see something like this which indicate a dependency's missing.
Dependencies is missing for Dotnet Core 6.0
Execute ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.
```
You can easily correct the problem by executing `./bin/installdependencies.sh`.
The `installdependencies.sh` script should install all required dependencies on all supported Linux versions
> Note: The `installdependencies.sh` script will try to use the default package management mechanism on your Linux flavor (ex. `yum`/`apt-get`/`apt`).
### Full dependencies list
> **Note:** The `installdependencies.sh` script will try to use the default package management mechanism on your Linux flavor (ex. `yum`/`apt-get`/`apt`).
Debian based OS (Debian, Ubuntu, Linux Mint)
## Manual Dependency Installation
If the automatic installation doesn't work, you can manually install dependencies using your package manager:
### Debian based OS (Debian, Ubuntu, Linux Mint)
```bash
sudo apt-get update
sudo apt-get install -y liblttng-ust1 libkrb5-3 zlib1g libssl1.1 libicu66
```
**Required packages:**
- liblttng-ust1 or liblttng-ust0
- libkrb5-3
- zlib1g
- libssl1.1, libssl1.0.2 or libssl1.0.0
- libicu63, libicu60, libicu57 or libicu55
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
### Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
```bash
sudo yum install -y lttng-ust openssl-libs krb5-libs zlib libicu
# Or for newer systems:
sudo dnf install -y lttng-ust openssl-libs krb5-libs zlib libicu
```
**Required packages:**
- lttng-ust
- openssl-libs
- krb5-libs
- zlib
- libicu
SUSE based OS (OpenSUSE, SUSE Enterprise)
### SUSE based OS (OpenSUSE, SUSE Enterprise)
```bash
sudo zypper install -y lttng-ust libopenssl1_1 krb5 zlib libicu60_2
```
**Required packages:**
- lttng-ust
- libopenssl1_1
- krb5
- zlib
- libicu60_2
## [More .Net Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/linux-prerequisites?tabs=netcore2x)
## Troubleshooting
### Common Issues
**Permission denied errors:**
```bash
sudo chmod +x ./config.sh ./run.sh
```
**Missing dependencies after installation:**
```bash
# Check what's missing
ldd ./bin/Runner.Listener
# Reinstall dependencies
./bin/installdependencies.sh
```
**SSL/TLS errors:**
```bash
# Update certificates
sudo apt-get update && sudo apt-get install ca-certificates
# Or for RHEL/CentOS:
sudo yum update ca-certificates
```
### Getting Help
- Check our [troubleshooting guide](../checks/README.md)
- Search [GitHub Community Discussions](https://github.com/orgs/community/discussions/categories/actions)
- Review [common network issues](../checks/network.md)
## [More .NET Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/linux-prerequisites?tabs=netcore2x)

View File

@@ -1,9 +1,136 @@
# ![osx](../res/apple_med.png) macOS/OS X System Prerequisites
# ![osx](../res/apple_med.png) macOS System Prerequisites
## Supported Versions
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)."
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/reference/runners/self-hosted-runners#macos)."
## [More .Net Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/macos-prerequisites?tabs=netcore30)
## Quick Setup
1. **Download** the latest runner from [releases](https://github.com/actions/runner/releases)
2. **Extract** the downloaded archive: `tar xzf actions-runner-osx-x64-*.tar.gz`
3. **Run** `./config.sh` to configure the runner
4. **Install** as a service: `sudo ./svc.sh install` and `sudo ./svc.sh start`
## System Requirements
### macOS Version
- macOS 10.15 (Catalina) or later
- Both Intel (x64) and Apple Silicon (ARM64) are supported
### Required Tools
#### Homebrew (Recommended)
Install Homebrew for easy package management:
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
#### Development Tools
```bash
# Install Xcode Command Line Tools
xcode-select --install
# Install essential development tools via Homebrew
brew install git curl wget
```
### .NET Runtime
- .NET 6.0 runtime (automatically included with the runner)
## Setup Steps
### 1. Download and Extract
```bash
# Create runner directory
mkdir ~/actions-runner && cd ~/actions-runner
# Download latest release (replace with actual version)
curl -O -L https://github.com/actions/runner/releases/download/v2.xyz.z/actions-runner-osx-x64-2.xyz.z.tar.gz
# Extract
tar xzf ./actions-runner-osx-x64-2.xyz.z.tar.gz
```
### 2. Configure
```bash
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO --token YOUR_TOKEN
```
### 3. Run as Service (macOS)
```bash
# Install as launchd service
sudo ./svc.sh install
# Start the service
sudo ./svc.sh start
# Check status
sudo ./svc.sh status
```
### 4. Run Interactively (Alternative)
```bash
./run.sh
```
## macOS-Specific Considerations
### Security & Privacy
- Allow the runner executable through macOS Gatekeeper
- Grant necessary permissions in System Preferences > Security & Privacy
### Code Signing
For repositories that build macOS applications:
```bash
# Install certificates for code signing
security import developer_certificates.p12 -k ~/Library/Keychains/login.keychain
```
### Xcode Integration
If building iOS/macOS apps:
```bash
# Install Xcode from App Store or developer portal
# Set Xcode path
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
```
## Troubleshooting
### Common Issues
**Permission Denied:**
```bash
chmod +x ./config.sh ./run.sh ./svc.sh
```
**Gatekeeper Issues:**
```bash
# Allow unsigned binary to run
sudo spctl --master-disable
# Or specifically allow the runner
sudo spctl --add ./bin/Runner.Listener
```
**Service Not Starting:**
```bash
# Check system logs
sudo ./svc.sh status
tail -f ~/Library/Logs/Runner_*.log
```
**Path Issues:**
```bash
# Ensure proper PATH in your shell profile
echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```
### Getting Help
- Check our [troubleshooting guide](../checks/README.md)
- Review [common network issues](../checks/network.md)
- Search [GitHub Community Discussions](https://github.com/orgs/community/discussions/categories/actions)
## [More .NET Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/macos-prerequisites?tabs=netcore30)

View File

@@ -2,6 +2,94 @@
## Supported Versions
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)."
Please see "[Supported architectures and operating systems for self-hosted runners](https://docs.github.com/en/actions/reference/runners/self-hosted-runners#windows)."
## [More .NET Core Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore30)
## Quick Setup
1. **Download** the latest runner from [releases](https://github.com/actions/runner/releases)
2. **Extract** the downloaded archive to your desired directory
3. **Run** `config.cmd` as Administrator to configure the runner
4. **Install** as a service (optional): `svc.sh install` and `svc.sh start`
## System Requirements
### .NET Runtime
- .NET 6.0 runtime (automatically installed with the runner)
- Windows PowerShell 5.1 or PowerShell Core 6.0+
### Windows Features
Windows runners require the following components:
```powershell
# Enable required Windows features (run as Administrator)
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
```
### Visual Studio Build Tools (For builds requiring compilation)
For repositories that need to compile code, install:
- **Visual Studio 2017 or newer** [Install here](https://visualstudio.microsoft.com)
- **Visual Studio 2022 17.3 Preview or later** (for ARM64) [Install here](https://docs.microsoft.com/en-us/visualstudio/releases/2022/release-notes-preview)
### Git for Windows
- **Git for Windows** [Install here](https://git-scm.com/downloads) (required for repository operations)
## Common Setup Steps
### 1. Create Runner Directory
```cmd
mkdir C:\actions-runner
cd C:\actions-runner
```
### 2. Download and Extract
```powershell
# Download latest release
Invoke-WebRequest -Uri "https://github.com/actions/runner/releases/download/v2.xyz.z/actions-runner-win-x64-2.xyz.z.zip" -OutFile "actions-runner.zip"
# Extract
Expand-Archive -Path "actions-runner.zip" -DestinationPath "."
```
### 3. Configure
```cmd
.\config.cmd --url https://github.com/YOUR_ORG/YOUR_REPO --token YOUR_TOKEN
```
### 4. Run as Service
```cmd
# Install service
.\svc.sh install
# Start service
.\svc.sh start
```
## Troubleshooting
### Common Issues
**PowerShell Execution Policy:**
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
**Windows Defender/Antivirus:**
- Add runner directory to antivirus exclusions
- Exclude `Runner.Listener.exe` and `Runner.Worker.exe`
**Firewall Issues:**
```powershell
# Allow runner through Windows Firewall
New-NetFirewallRule -DisplayName "GitHub Actions Runner" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow
```
**Permission Issues:**
- Run `config.cmd` as Administrator
- Ensure the runner user has "Log on as a service" rights
### Getting Help
- Check our [troubleshooting guide](../checks/README.md)
- Review [common issues](../checks/actions.md)
- Search [GitHub Community Discussions](https://github.com/orgs/community/discussions/categories/actions)
## [More .NET Prerequisites Information](https://docs.microsoft.com/en-us/dotnet/core/windows-prerequisites?tabs=netcore30)

View File

@@ -1,13 +1,20 @@
## What's Changed
* Try add orchestrationid into user-agent using token claim. by @TingluoHuang in https://github.com/actions/runner/pull/3945
* Fix null reference exception in user agent handling by @salmanmkc in https://github.com/actions/runner/pull/3946
* Runner Support for executing Node24 Actions by @salmanmkc in https://github.com/actions/runner/pull/3940
* Update dotnet sdk to latest version @8.0.412 by @github-actions[bot] in https://github.com/actions/runner/pull/3941
* Update Docker to v28.3.2 and Buildx to v0.26.1 by @github-actions[bot] in https://github.com/actions/runner/pull/3953
* Fix if statement structure in update script and variable reference by @salmanmkc in https://github.com/actions/runner/pull/3956
* Add V2 flow for runner deletion by @Samirat in https://github.com/actions/runner/pull/3954
* Node 20 -> Node 24 migration feature flagging, opt-in and opt-out environment variables by @salmanmkc in https://github.com/actions/runner/pull/3948
* Update Node20 and Node24 to latest by @djs-intel in https://github.com/actions/runner/pull/3972
* Redirect supported OS doc section to current public Docs location by @corycalahan in https://github.com/actions/runner/pull/3979
* Bump Microsoft.NET.Test.Sdk from 17.13.0 to 17.14.1 by @dependabot[bot] in https://github.com/actions/runner/pull/3975
* Bump Azure.Storage.Blobs from 12.24.0 to 12.25.0 by @dependabot[bot] in https://github.com/actions/runner/pull/3974
* Bump actions/download-artifact from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/3973
* Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/3982
## New Contributors
* @salmanmkc made their first contribution in https://github.com/actions/runner/pull/3946
* @Samirat made their first contribution in https://github.com/actions/runner/pull/3954
* @djs-intel made their first contribution in https://github.com/actions/runner/pull/3972
**Full Changelog**: https://github.com/actions/runner/compare/v2.326.0...v2.327.0
**Full Changelog**: https://github.com/actions/runner/compare/v2.327.1...v2.328.0
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.

View File

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

View File

@@ -123,8 +123,7 @@ fi
# fix upgrade issue with macOS when running as a service
attemptedtargetedfix=0
currentplatform=$(uname | awk '{print tolower($0)}')
if [[ "$currentplatform" == 'darwin' && $restartinteractiverunner -eq 0 ]];
then
if [[ "$currentplatform" == 'darwin' && $restartinteractiverunner -eq 0 ]]; then
# We needed a fix for https://github.com/actions/runner/issues/743
# We will recreate the ./externals/nodeXY/bin/node of the past runner version that launched the runnerlistener service
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
@@ -218,4 +217,4 @@ if [ $restartinteractiverunner -ne 0 ]
then
date "+[%F %T-%4N] Restarting interactive runner" >> "$logfile.succeed" 2>&1
"$rootfolder/run.sh" &
fi
fi

View File

@@ -170,6 +170,22 @@ namespace GitHub.Runner.Common
public static readonly string AddCheckRunIdToJobContext = "actions_add_check_run_id_to_job_context";
public static readonly string DisplayHelpfulActionsDownloadErrors = "actions_display_helpful_actions_download_errors";
}
// 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 Guid TelemetryRecordId = new Guid("11111111-1111-1111-1111-111111111111");

View File

@@ -19,6 +19,7 @@ namespace GitHub.Runner.Common
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 DeleteRunnerAsync(string githubUrl, string githubToken, ulong runnerId);
Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken);
}
@@ -43,117 +44,15 @@ namespace GitHub.Runner.Common
public async Task<List<TaskAgent>> GetRunnerByNameAsync(string githubUrl, string githubToken, string 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 githubApiUrl = $"{GetEntityUrl(githubUrl)}/runners?name={Uri.EscapeDataString(agentName)}";
var runnersList = await RetryRequest<ListRunnersResponse>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
return runnersList.ToTaskAgents();
}
public async Task<List<TaskAgentPool>> GetRunnerGroupsAsync(string githubUrl, string githubToken)
{
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 githubApiUrl = $"{GetEntityUrl(githubUrl)}/runner-groups";
var agentPools = await RetryRequest<RunnerGroupList>(githubApiUrl, githubToken, RequestType.Get, 3, "Failed to get agents pools");
return agentPools?.ToAgentPoolList();
}
@@ -204,6 +103,12 @@ namespace GitHub.Runner.Common
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)
{
int retry = 0;
@@ -220,13 +125,22 @@ namespace GitHub.Runner.Common
try
{
HttpResponseMessage response = null;
if (requestType == RequestType.Get)
switch (requestType)
{
response = await httpClient.GetAsync(githubApiUrl);
}
else
{
response = await httpClient.PostAsync(githubApiUrl, body);
case RequestType.Get:
response = await httpClient.GetAsync(githubApiUrl);
break;
case RequestType.Post:
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)
@@ -261,5 +175,61 @@ namespace GitHub.Runner.Common
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;
}
}
}

View File

@@ -1,10 +1,33 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common.Util
{
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";
public static readonly ReadOnlyCollection<string> BuiltInNodeVersions = new(new[] { "node20" });
public static string GetInternalNodeVersion()
@@ -18,6 +41,70 @@ namespace GitHub.Runner.Common.Util
}
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>
/// Checks if Node24 is requested but running on ARM32 Linux, and determines if fallback is needed.
@@ -26,14 +113,50 @@ namespace GitHub.Runner.Common.Util
/// <returns>A tuple containing the adjusted node version and an optional warning message</returns>
public static (string nodeVersion, string warningMessage) CheckNodeVersionForLinuxArm32(string preferredVersion)
{
if (string.Equals(preferredVersion, "node24", StringComparison.OrdinalIgnoreCase) &&
if (string.Equals(preferredVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase) &&
Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.Arm) &&
Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
{
return ("node20", "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
return (Constants.Runner.NodeMigration.Node20, "Node 24 is not supported on Linux ARM32 platforms. Falling back to Node 20.");
}
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;
}
}
}

View File

@@ -537,41 +537,50 @@ namespace GitHub.Runner.Listener.Configuration
if (isConfigured && hasCredentials)
{
RunnerSettings settings = _store.GetSettings();
var credentialManager = HostContext.GetService<ICredentialManager>();
// Get the credentials
VssCredentials creds = null;
if (string.IsNullOrEmpty(settings.GitHubUrl))
{
var credProvider = GetCredentialProvider(command, settings.ServerUrl);
creds = credProvider.GetVssCredentials(HostContext, allowAuthUrlV2: false);
Trace.Info("legacy vss cred retrieved");
}
else
if (settings.UseV2Flow)
{
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);
await _dotcomServer.DeleteRunnerAsync(settings.GitHubUrl, deletionToken, settings.AgentId);
}
else
{
await _runnerServer.DeleteAgentAsync(settings.AgentId);
var credentialManager = HostContext.GetService<ICredentialManager>();
_term.WriteLine();
_term.WriteSuccessMessage("Runner removed successfully");
// Get the credentials
VssCredentials creds = null;
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();
_term.WriteSuccessMessage("Runner removed successfully");
}
else
{

View File

@@ -58,10 +58,41 @@ namespace GitHub.Runner.Worker.Handlers
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 node20 EoL approaching, we're preparing to migrate to node24
if (string.Equals(nodeData.NodeVersion, "node12", StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(nodeData.NodeVersion, "node16", StringComparison.InvariantCultureIgnoreCase))
{
nodeData.NodeVersion = "node20";
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.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;

View File

@@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.24.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.25.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />

View File

@@ -978,7 +978,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_messageListener.Verify(x => x.GetNextMessageAsync(It.IsAny<CancellationToken>()), Times.AtLeast(2));
_messageListener.Verify(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()), Times.AtLeast(2));
_messageListener.Verify(x => x.DeleteSessionAsync(), Times.Once());
_credentialManager.Verify(x => x.LoadCredentials(true), Times.Exactly(2));
_credentialManager.Verify(x => x.LoadCredentials(true), Times.AtLeast(2));
Assert.False(hc.AllowAuthMigration);
}

View File

@@ -1,793 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Linq;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Sdk;
using Xunit;
namespace GitHub.Runner.Common.Tests.Listener
{
public sealed class ShellScriptSyntaxL0
{
private void ValidateShellScriptTemplateSyntax(string relativePath, string templateName, bool shouldPass = true, Func<string, string> templateModifier = null, bool useFullPath = false, bool useShellCheck = true)
{
// Skip on Windows
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
using (var hc = new TestHostContext(this))
{
// Arrange
string templatePath;
if (useFullPath)
{
templatePath = templateName;
}
else
{
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
templatePath = Path.Combine(rootDirectory, relativePath, templateName);
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string tempScriptPath = Path.Combine(tempDir, Path.GetFileNameWithoutExtension(templateName));
string debugLogPath = Path.Combine(tempDir, "debug_log.txt");
string template = File.ReadAllText(templatePath);
if (templateModifier != null)
{
template = templateModifier(template);
}
string rootFolder = useFullPath ? Path.GetDirectoryName(templatePath) : Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
template = ReplaceCommonPlaceholders(template, rootFolder, tempDir);
File.WriteAllText(tempScriptPath, template);
var chmodProcess = new Process();
chmodProcess.StartInfo.FileName = "chmod";
chmodProcess.StartInfo.Arguments = $"+x {tempScriptPath}";
chmodProcess.Start();
chmodProcess.WaitForExit();
var bashCheckProcess = new Process();
bashCheckProcess.StartInfo.FileName = "/bin/bash";
bashCheckProcess.StartInfo.Arguments = $"-c \"bash -n {tempScriptPath}; echo $?\"";
bashCheckProcess.StartInfo.RedirectStandardOutput = true;
bashCheckProcess.StartInfo.RedirectStandardError = true;
bashCheckProcess.StartInfo.UseShellExecute = false;
bashCheckProcess.Start();
string bashCheckOutput = bashCheckProcess.StandardOutput.ReadToEnd();
string bashCheckErrors = bashCheckProcess.StandardError.ReadToEnd();
bashCheckProcess.WaitForExit();
// Act - Check syntax using bash -n
var process = new Process();
process.StartInfo.FileName = "bash";
process.StartInfo.Arguments = $"-n {tempScriptPath}";
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.Start();
string errors = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrEmpty(errors))
{
Console.WriteLine($"Errors: {errors}");
}
// Assert based on expected outcome
if (shouldPass)
{
Console.WriteLine("Test expected to pass, checking exit code and errors");
Assert.Equal(0, process.ExitCode);
Assert.Empty(errors);
if (shouldPass && process.ExitCode == 0 && useShellCheck)
{
RunShellCheck(tempScriptPath);
}
}
else
{
Console.WriteLine("Test expected to fail, checking exit code and errors");
Assert.NotEqual(0, process.ExitCode);
Assert.NotEmpty(errors);
}
// Cleanup - But leave the temp directory for debugging on failure
if (process.ExitCode == 0 && shouldPass)
{
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
else
{
Console.WriteLine($"Not cleaning up temp directory for debugging: {tempDir}");
}
}
}
catch (Exception ex)
{
Assert.Fail($"Exception during test for {templateName}: {ex}");
}
}
private void RunShellCheck(string scriptPath)
{
var shellcheckExistsProcess = new Process();
shellcheckExistsProcess.StartInfo.FileName = "which";
shellcheckExistsProcess.StartInfo.Arguments = "shellcheck";
shellcheckExistsProcess.StartInfo.RedirectStandardOutput = true;
shellcheckExistsProcess.StartInfo.UseShellExecute = false;
shellcheckExistsProcess.Start();
string shellcheckPath = shellcheckExistsProcess.StandardOutput.ReadToEnd().Trim();
shellcheckExistsProcess.WaitForExit();
if (!string.IsNullOrEmpty(shellcheckPath))
{
Console.WriteLine("ShellCheck found, performing additional validation");
var shellcheckProcess = new Process();
shellcheckProcess.StartInfo.FileName = "shellcheck";
shellcheckProcess.StartInfo.Arguments = $"-e SC2001,SC2002,SC2006,SC2009,SC2016,SC2034,SC2039,SC2046,SC2048,SC2059,SC2086,SC2094,SC2115,SC2116,SC2126,SC2129,SC2140,SC2145,SC2153,SC2154,SC2155,SC2162,SC2164,SC2166,SC2174,SC2181,SC2206,SC2207,SC2221,SC2222,SC2230,SC2236,SC2242,SC2268 {scriptPath}";
shellcheckProcess.StartInfo.RedirectStandardOutput = true;
shellcheckProcess.StartInfo.RedirectStandardError = true;
shellcheckProcess.StartInfo.UseShellExecute = false;
shellcheckProcess.Start();
string shellcheckOutput = shellcheckProcess.StandardOutput.ReadToEnd();
string shellcheckErrors = shellcheckProcess.StandardError.ReadToEnd();
shellcheckProcess.WaitForExit();
if (shellcheckProcess.ExitCode != 0)
{
Console.WriteLine($"ShellCheck found syntax errors: {shellcheckOutput}");
Console.WriteLine($"ShellCheck errors: {shellcheckErrors}");
Assert.Fail($"ShellCheck validation failed with exit code {shellcheckProcess.ExitCode}. Output: {shellcheckOutput}. Errors: {shellcheckErrors}");
}
else
{
Console.WriteLine("ShellCheck validation passed");
}
}
else
{
Console.WriteLine("ShellCheck not found, skipping additional validation");
}
}
private string ReplaceCommonPlaceholders(string template, string rootDirectory, string tempDir)
{
template = template.Replace("_PROCESS_ID_", "1234");
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener");
template = template.Replace("_ROOT_FOLDER_", rootDirectory);
template = template.Replace("_EXIST_RUNNER_VERSION_", "2.300.0");
template = template.Replace("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
template = template.Replace("_SERVICEUSERNAME_", "runner");
template = template.Replace("_SERVICEPASSWORD_", "password");
template = template.Replace("_SERVICEDISPLAYNAME_", "GitHub Actions Runner");
template = template.Replace("_SERVICENAME_", "github-runner");
template = template.Replace("_SERVICELOGPATH_", Path.Combine(tempDir, "service.log"));
template = template.Replace("_RUNNERSERVICEUSERDISPLAYNAME_", "GitHub Actions Runner Service");
return template;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void UpdateShTemplateHasValidSyntax()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "update.sh.template");
}
catch (Exception ex)
{
Console.WriteLine($"Error during test: {ex}");
throw;
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void DarwinSvcShTemplateHasValidSyntax()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "darwin.svc.sh.template");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void DarwinSvcShTemplateWithErrorsFailsValidation()
{
ValidateShellScriptTemplateSyntax(
"src/Misc/layoutbin",
"darwin.svc.sh.template",
shouldPass: false,
templateModifier: template =>
{
template = template.Replace("fi\n", "\n");
template = template.Replace("esac", "");
template = template.Replace("\"$svcuser\"", "\"$svcuser");
return template;
});
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void SystemdSvcShTemplateHasValidSyntax()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "systemd.svc.sh.template");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void SystemdSvcShTemplateWithErrorsFailsValidation()
{
ValidateShellScriptTemplateSyntax(
"src/Misc/layoutbin",
"systemd.svc.sh.template",
shouldPass: false,
templateModifier: template =>
{
template = template.Replace("done\n", "\n");
template = template.Replace("function", "function (");
template = template.Replace("if [ ! -f ", "if ! -f ");
return template;
});
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void RunHelperShTemplateHasValidSyntax()
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutroot", "run-helper.sh.template");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void RunHelperShTemplateWithErrorsFailsValidation()
{
ValidateShellScriptTemplateSyntax(
"src/Misc/layoutroot",
"run-helper.sh.template",
shouldPass: false,
templateModifier: template =>
{
template = template.Replace("${RUNNER_ROOT}", "${RUNNER_ROOT");
template = template.Replace("\"$@\"", "\"$@");
template = template.Replace("> /dev/null", ">> >>");
return template;
});
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void ValidateShellScript_MissingTemplate_ThrowsException()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
ValidateShellScriptTemplateSyntax("src/Misc/layoutbin", "non_existent_template.sh.template", shouldPass: true);
Assert.Fail("Expected exception was not thrown");
}
catch (Exception ex)
{
Assert.Contains("non_existent_template.sh.template", ex.Message);
Assert.Contains("FileNotFoundException", ex.Message);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "windows")]
public void ValidateShellScript_ComplexScript_ValidatesCorrectly()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
// Create a test template with complex shell scripting patterns
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string templatePath = Path.Combine(tempDir, "complex_shell.sh.template");
// Write a sample template with various shell features
string template = @"#!/bin/bash
set -e
# Function with nested quotes and complex syntax
function complex_func() {
local var1=""$1""
local var2=""${2:-default}""
echo ""Function arguments: '$var1' and '$var2'""
if [ ""$var1"" == ""test"" ]; then
echo ""This is a 'test' with nested quotes""
fi
}
# Complex variable substitutions
VAR1=""test value""
VAR2=""${VAR1:0:4}""
VAR3=""$(echo ""command substitution"")""
# Here document
cat << EOF > /tmp/testfile
This is a test file
With multiple lines
And some $VAR1 substitution
EOF
complex_func ""test"" ""value""
exit 0";
File.WriteAllText(templatePath, template);
try
{
ValidateShellScriptTemplateSyntax("", templatePath, shouldPass: true, useFullPath: true);
}
finally
{
// Clean up
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void UpdateCmdTemplateHasValidSyntax()
{
// Skip on non-Windows platforms
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: true);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void UpdateCmdTemplateWithErrorsFailsValidation()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
ValidateCmdScriptTemplateSyntax("update.cmd.template", shouldPass: false,
templateModifier: template =>
{
template = template.Replace("if exist", "if exist (");
template = template.Replace("echo", "echo \"Unclosed quote");
return template;
});
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void ValidateCmdScript_MissingTemplate_ThrowsFileNotFoundException()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
try
{
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
string templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", "non_existent_template.cmd.template");
string content = File.ReadAllText(templatePath);
Assert.Fail($"Expected FileNotFoundException was not thrown for {templatePath}");
}
catch (FileNotFoundException)
{
// This is expected, so test passes
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void ValidateCmdScript_ComplexQuoting_ValidatesCorrectly()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string templatePath = Path.Combine(tempDir, "complex_quotes.cmd.template");
string template = @"@echo off
echo ""This has ""nested"" quotes""
echo ""This has an escaped quote: \""test\""""
echo Simple command
if ""quoted condition"" == ""quoted condition"" (
echo ""Inside if block with quotes""
)";
File.WriteAllText(templatePath, template);
try
{
ValidateCmdScriptTemplateSyntax(templatePath, shouldPass: true, useFullPath: true);
}
finally
{
// Clean up
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Runner")]
[Trait("SkipOn", "osx,linux")]
public void ValidateCmdScript_ComplexParentheses_ValidatesCorrectly()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string templatePath = Path.Combine(tempDir, "complex_parens.cmd.template");
string template = @"@echo off
echo Text with (parentheses)
echo ""Text with (parentheses inside quotes)""
if exist file.txt (
if exist other.txt (
echo Nested if blocks
) else (
echo Nested else
)
) else (
echo Outer else
)";
File.WriteAllText(templatePath, template);
try
{
ValidateCmdScriptTemplateSyntax(templatePath, shouldPass: true, useFullPath: true);
}
finally
{
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
private bool HasUnclosedQuotes(string text)
{
bool inQuote = false;
bool isEscaped = false;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (c == '\\')
{
isEscaped = !isEscaped;
continue;
}
if (c == '"' && !isEscaped)
{
inQuote = !inQuote;
}
if (c != '\\')
{
isEscaped = false;
}
}
return inQuote;
}
private bool HasBalancedParentheses(string text)
{
int balance = 0;
bool inQuote = false;
bool isEscaped = false;
bool inComment = false;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (inComment)
{
if (c == '\n' || c == '\r')
{
inComment = false;
}
continue;
}
if (!inQuote && i < text.Length - 1 && c == ':' && text[i+1] == ':')
{
inComment = true;
continue;
}
if (!inQuote && i < text.Length - 2 && c == 'r' && text[i+1] == 'e' && text[i+2] == 'm' &&
(i == 0 || char.IsWhiteSpace(text[i-1])))
{
inComment = true;
continue;
}
if (c == '\\')
{
isEscaped = !isEscaped;
continue;
}
if (c == '"' && !isEscaped)
{
inQuote = !inQuote;
}
if (!inQuote)
{
if (c == '(')
{
balance++;
}
else if (c == ')')
{
balance--;
if (balance < 0)
{
return false;
}
}
}
if (c != '\\')
{
isEscaped = false;
}
}
return balance == 0;
}
private void ValidateCmdScriptTemplateSyntax(string templateName, bool shouldPass, Func<string, string> templateModifier = null, bool useFullPath = false)
{
try
{
using (var hc = new TestHostContext(this))
{
// Arrange
string templatePath;
if (useFullPath)
{
templatePath = templateName;
}
else
{
string rootDirectory = Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
templatePath = Path.Combine(rootDirectory, "src", "Misc", "layoutbin", templateName);
}
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
string tempUpdatePath = Path.Combine(tempDir, Path.GetFileName(templatePath).Replace(".template", ""));
string template = File.ReadAllText(templatePath);
if (templateModifier != null)
{
template = templateModifier(template);
}
template = template.Replace("_PROCESS_ID_", "1234");
template = template.Replace("_RUNNER_PROCESS_NAME_", "Runner.Listener.exe");
string rootFolder = useFullPath ? Path.GetDirectoryName(templatePath) : Path.GetFullPath(Path.Combine(TestUtil.GetSrcPath(), ".."));
template = template.Replace("_ROOT_FOLDER_", rootFolder);
template = template.Replace("_EXIST_RUNNER_VERSION_", "2.300.0");
template = template.Replace("_DOWNLOAD_RUNNER_VERSION_", "2.301.0");
template = template.Replace("_UPDATE_LOG_", Path.Combine(tempDir, "update.log"));
template = template.Replace("_RESTART_INTERACTIVE_RUNNER_", "0");
File.WriteAllText(tempUpdatePath, template);
string errors = string.Empty;
string output = string.Empty;
int exitCode = 0;
try
{
string testBatchFile = Path.Combine(tempDir, "test.cmd");
File.WriteAllText(testBatchFile, "@echo off\r\nexit /b 0");
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/c \"cd /d \"{tempDir}\" && echo Script syntax check && exit /b 0\"";
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = tempDir;
process.Start();
output = process.StandardOutput.ReadToEnd();
errors = process.StandardError.ReadToEnd();
process.WaitForExit();
exitCode = process.ExitCode;
}
catch (Exception ex)
{
errors = ex.ToString();
exitCode = 1;
}
bool hasMissingParenthesis = !HasBalancedParentheses(template);
bool hasUnclosedQuotes = HasUnclosedQuotes(template);
bool hasOutputErrors = !string.IsNullOrEmpty(errors) ||
output.Contains("syntax error") ||
output.Contains("not recognized") ||
output.Contains("unexpected") ||
output.Contains("Syntax check failed");
bool hasInvalidSyntaxPatterns = false;
if (template.Contains("if") && !template.Contains("if "))
{
hasInvalidSyntaxPatterns = true;
}
if (template.Contains("goto") && !template.Contains("goto "))
{
hasInvalidSyntaxPatterns = true;
}
if (template.Contains("(") && !template.Contains(")"))
{
hasInvalidSyntaxPatterns = true;
}
bool staticAnalysisPassed = !hasMissingParenthesis &&
!hasUnclosedQuotes &&
!hasInvalidSyntaxPatterns;
bool executionPassed = true;
try
{
if (!errors.Contains("filename, directory name, or volume label syntax"))
{
executionPassed = exitCode == 0 && !hasOutputErrors;
}
}
catch
{
executionPassed = true;
}
bool validationPassed = staticAnalysisPassed && executionPassed;
if (shouldPass)
{
Assert.True(validationPassed,
$"Template validation should have passed but failed. Exit code: {exitCode}, " +
$"Errors: {errors}, HasMissingParenthesis: {hasMissingParenthesis}, " +
$"HasUnclosedQuotes: {hasUnclosedQuotes}");
}
else
{
Assert.False(validationPassed,
"Template validation should have failed but passed. " +
"The intentionally introduced syntax errors were not detected.");
}
// Cleanup
try
{
Directory.Delete(tempDir, true);
}
catch
{
// Best effort cleanup
}
}
}
catch (Exception ex)
{
Assert.Fail($"Exception during test: {ex.ToString()}");
}
}
}
}

View File

@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using Xunit;
namespace GitHub.Runner.Common.Tests.Util
{
public class NodeUtilL0
{
// We're testing the logic with feature flags
[Theory]
[InlineData(false, false, false, false, "node20", false)] // Phase 1: No env vars
[InlineData(false, false, false, true, "node20", false)] // Phase 1: Allow unsecure (redundant)
[InlineData(false, false, true, false, "node24", false)] // Phase 1: Force node24
[InlineData(false, false, true, true, "node20", true)] // Phase 1: Both flags (use phase default + warning)
[InlineData(false, true, false, false, "node24", false)] // Phase 2: No env vars
[InlineData(false, true, false, true, "node20", false)] // Phase 2: Allow unsecure
[InlineData(false, true, true, false, "node24", false)] // Phase 2: Force node24 (redundant)
[InlineData(false, true, true, true, "node24", true)] // Phase 2: Both flags (use phase default + warning)
[InlineData(true, false, false, false, "node24", false)] // Phase 3: Always Node 24 regardless of env vars
[InlineData(true, false, false, true, "node24", false)] // Phase 3: Always Node 24 regardless of env vars
[InlineData(true, false, true, false, "node24", false)] // Phase 3: Always Node 24 regardless of env vars
[InlineData(true, false, true, true, "node24", false)] // Phase 3: Always Node 24 regardless of env vars, no warnings in Phase 3
public void TestNodeVersionLogic(bool requireNode24, bool useNode24ByDefault, bool forceNode24, bool allowUnsecureNode, string expectedVersion, bool expectWarning)
{
try
{
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.ForceNode24Variable, forceNode24 ? "true" : null);
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, allowUnsecureNode ? "true" : null);
// Call the actual method
var (actualVersion, warningMessage) = NodeUtil.DetermineActionsNodeVersion(null, useNode24ByDefault, requireNode24);
// Assert
Assert.Equal(expectedVersion, actualVersion);
if (expectWarning)
{
Assert.NotNull(warningMessage);
Assert.Contains("Both", warningMessage);
Assert.Contains("are set to true", warningMessage);
}
else
{
Assert.Null(warningMessage);
}
}
finally
{
// Cleanup
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.ForceNode24Variable, null);
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, null);
}
}
[Theory]
[InlineData(false, false, false, false, false, true, "node20", false)] // Phase 1: System env: none, Workflow env: allow=true
[InlineData(false, false, true, false, false, false, "node24", false)] // Phase 1: System env: force node24, Workflow env: none
[InlineData(false, true, false, false, true, false, "node24", false)] // Phase 1: System env: none, Workflow env: force node24
[InlineData(false, false, false, true, false, true, "node20", false)] // Phase 1: System env: allow=true, Workflow env: allow=true (workflow takes precedence)
[InlineData(false, false, true, true, false, false, "node20", true)] // Phase 1: System env: both true, Workflow env: none (use phase default + warning)
[InlineData(false, false, false, false, true, true, "node20", true)] // Phase 1: System env: none, Workflow env: both (use phase default + warning)
[InlineData(true, false, false, false, false, false, "node24", false)] // Phase 2: System env: none, Workflow env: none
[InlineData(true, false, false, true, false, false, "node20", false)] // Phase 2: System env: allow=true, Workflow env: none
[InlineData(true, false, false, false, false, true, "node20", false)] // Phase 2: System env: none, Workflow env: allow unsecure
[InlineData(true, false, true, false, false, true, "node20", false)] // Phase 2: System env: force node24, Workflow env: allow unsecure
[InlineData(true, false, true, true, false, false, "node24", true)] // Phase 2: System env: both true, Workflow env: none (use phase default + warning)
[InlineData(true, false, false, false, true, true, "node24", true)] // Phase 2: System env: none, Workflow env: both (phase default + warning)
[InlineData(false, true, false, false, false, true, "node24", false)] // Phase 3: System env: none, Workflow env: allow=true (always Node 24 in Phase 3)
[InlineData(false, true, true, true, false, false, "node24", false)] // Phase 3: System env: both true, Workflow env: none (always Node 24 in Phase 3, no warning)
[InlineData(false, true, false, false, true, true, "node24", false)] // Phase 3: System env: none, Workflow env: both (always Node 24 in Phase 3, no warning)
public void TestNodeVersionLogicWithWorkflowEnvironment(bool useNode24ByDefault, bool requireNode24,
bool systemForceNode24, bool systemAllowUnsecure,
bool workflowForceNode24, bool workflowAllowUnsecure,
string expectedVersion, bool expectWarning)
{
try
{
// Set system environment variables
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.ForceNode24Variable, systemForceNode24 ? "true" : null);
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, systemAllowUnsecure ? "true" : null);
// Set workflow environment variables
var workflowEnv = new Dictionary<string, string>();
if (workflowForceNode24)
{
workflowEnv[Constants.Runner.NodeMigration.ForceNode24Variable] = "true";
}
if (workflowAllowUnsecure)
{
workflowEnv[Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable] = "true";
}
// Call the actual method with our test parameters
var (actualVersion, warningMessage) = NodeUtil.DetermineActionsNodeVersion(workflowEnv, useNode24ByDefault, requireNode24);
// Assert
Assert.Equal(expectedVersion, actualVersion);
if (expectWarning)
{
Assert.NotNull(warningMessage);
Assert.Contains("Both", warningMessage);
Assert.Contains("are set to true", warningMessage);
}
else
{
Assert.Null(warningMessage);
}
}
finally
{
// Cleanup
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.ForceNode24Variable, null);
Environment.SetEnvironmentVariable(Constants.Runner.NodeMigration.AllowUnsecureNodeVersionVariable, null);
}
}
}
}

View File

@@ -15,7 +15,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.7.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.7.0" />

View File

@@ -1 +1 @@
2.327.0
2.328.0