mirror of
https://github.com/actions/runner.git
synced 2025-12-11 12:57:05 +00:00
Compare commits
12 Commits
v2.169.1
...
users/eric
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc46497742 | ||
|
|
c7768d4a7b | ||
|
|
70729fb3c4 | ||
|
|
1470a3b6e2 | ||
|
|
2fadf430e4 | ||
|
|
f798f5606b | ||
|
|
3f7a01af93 | ||
|
|
d5c54f9819 | ||
|
|
9f78ad3b34 | ||
|
|
97883c8cd5 | ||
|
|
c5fa9fb062 | ||
|
|
b2dcdc21dc |
57
docs/automate.md
Normal file
57
docs/automate.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Automate Configuring Self-Hosted Runners
|
||||||
|
|
||||||
|
|
||||||
|
## Export PAT
|
||||||
|
|
||||||
|
Before running any of these sample scripts, create a GitHub PAT and export it before running the script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RUNNER_CFG_PAT=yourPAT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create running as a service
|
||||||
|
|
||||||
|
**Scenario**: Run on a machine or VM (not container) which automates:
|
||||||
|
|
||||||
|
- Resolving latest released runner
|
||||||
|
- Download and extract latest
|
||||||
|
- Acquire a registration token
|
||||||
|
- Configure the runner
|
||||||
|
- Run as a systemd (linux) or Launchd (osx) service
|
||||||
|
|
||||||
|
:point_right: [Sample script here](../scripts/create-latest-svc.sh) :point_left:
|
||||||
|
|
||||||
|
Run as a one-liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level)
|
||||||
|
```bash
|
||||||
|
curl -s https://raw.githubusercontent.com/actions/runner/automate/scripts/create-latest-svc.sh | bash -s yourorg/yourrepo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstall running as service
|
||||||
|
|
||||||
|
**Scenario**: Run on a machine or VM (not container) which automates:
|
||||||
|
|
||||||
|
- Stops and uninstalls the systemd (linux) or Launchd (osx) service
|
||||||
|
- Acquires a removal token
|
||||||
|
- Removes the runner
|
||||||
|
|
||||||
|
:point_right: [Sample script here](../scripts/remove-svc.sh) :point_left:
|
||||||
|
|
||||||
|
Repo level one liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level)
|
||||||
|
```bash
|
||||||
|
curl -s https://raw.githubusercontent.com/actions/runner/automate/scripts/remove-svc.sh | bash -s yourorg/yourrepo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete an offline runner
|
||||||
|
|
||||||
|
**Scenario**: Deletes a registered runner that is offline:
|
||||||
|
|
||||||
|
- Ensures the runner is offline
|
||||||
|
- Resolves id from name
|
||||||
|
- Deletes the runner
|
||||||
|
|
||||||
|
:point_right: [Sample script here](../scripts/delete.sh) :point_left:
|
||||||
|
|
||||||
|
Repo level one-liner. NOTE: replace with yourorg/yourrepo (repo level) or just yourorg (org level) and replace runnername
|
||||||
|
```bash
|
||||||
|
curl -s https://raw.githubusercontent.com/actions/runner/automate/scripts/delete.sh | bash -s yourorg/yourrepo runnername
|
||||||
|
```
|
||||||
@@ -40,7 +40,7 @@ Debian based OS (Debian, Ubuntu, Linux Mint)
|
|||||||
- libssl1.1, libssl1.0.2 or libssl1.0.0
|
- libssl1.1, libssl1.0.2 or libssl1.0.0
|
||||||
- libicu63, libicu60, libicu57 or libicu55
|
- libicu63, libicu60, libicu57 or libicu55
|
||||||
|
|
||||||
Fedora based OS (Fedora, Redhat, Centos, Oracle Linux 7)
|
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)
|
||||||
|
|
||||||
- lttng-ust
|
- lttng-ust
|
||||||
- openssl-libs
|
- openssl-libs
|
||||||
|
|||||||
@@ -8,13 +8,15 @@
|
|||||||
- N/A
|
- N/A
|
||||||
|
|
||||||
## Windows x64
|
## Windows x64
|
||||||
We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows
|
We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows.
|
||||||
```
|
|
||||||
// Create a folder under the drive root
|
The following snipped needs to be run on `powershell`:
|
||||||
|
``` powershell
|
||||||
|
# Create a folder under the drive root
|
||||||
mkdir \actions-runner ; cd \actions-runner
|
mkdir \actions-runner ; cd \actions-runner
|
||||||
// Download the latest runner package
|
# Download the latest runner package
|
||||||
Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-win-x64-<RUNNER_VERSION>.zip -OutFile actions-runner-win-x64-<RUNNER_VERSION>.zip
|
Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-win-x64-<RUNNER_VERSION>.zip -OutFile actions-runner-win-x64-<RUNNER_VERSION>.zip
|
||||||
// Extract the installer
|
# Extract the installer
|
||||||
Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
||||||
[System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD\actions-runner-win-x64-<RUNNER_VERSION>.zip", "$PWD")
|
[System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD\actions-runner-win-x64-<RUNNER_VERSION>.zip", "$PWD")
|
||||||
```
|
```
|
||||||
@@ -22,44 +24,44 @@ Add-Type -AssemblyName System.IO.Compression.FileSystem ;
|
|||||||
## OSX
|
## OSX
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
// Create a folder
|
# Create a folder
|
||||||
mkdir actions-runner && cd actions-runner
|
mkdir actions-runner && cd actions-runner
|
||||||
// Download the latest runner package
|
# Download the latest runner package
|
||||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||||
// Extract the installer
|
# Extract the installer
|
||||||
tar xzf ./actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
tar xzf ./actions-runner-osx-x64-<RUNNER_VERSION>.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
## Linux x64
|
## Linux x64
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
// Create a folder
|
# Create a folder
|
||||||
mkdir actions-runner && cd actions-runner
|
mkdir actions-runner && cd actions-runner
|
||||||
// Download the latest runner package
|
# Download the latest runner package
|
||||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||||
// Extract the installer
|
# Extract the installer
|
||||||
tar xzf ./actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
tar xzf ./actions-runner-linux-x64-<RUNNER_VERSION>.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
## Linux arm64 (Pre-release)
|
## Linux arm64 (Pre-release)
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
// Create a folder
|
# Create a folder
|
||||||
mkdir actions-runner && cd actions-runner
|
mkdir actions-runner && cd actions-runner
|
||||||
// Download the latest runner package
|
# Download the latest runner package
|
||||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||||
// Extract the installer
|
# Extract the installer
|
||||||
tar xzf ./actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
tar xzf ./actions-runner-linux-arm64-<RUNNER_VERSION>.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
## Linux arm (Pre-release)
|
## Linux arm (Pre-release)
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
// Create a folder
|
# Create a folder
|
||||||
mkdir actions-runner && cd actions-runner
|
mkdir actions-runner && cd actions-runner
|
||||||
// Download the latest runner package
|
# Download the latest runner package
|
||||||
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
curl -O -L https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||||
// Extract the installer
|
# Extract the installer
|
||||||
tar xzf ./actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
tar xzf ./actions-runner-linux-arm-<RUNNER_VERSION>.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
4
scripts/README.md
Normal file
4
scripts/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Sample scripts for self-hosted runners
|
||||||
|
|
||||||
|
Here are some examples to work from if you'd like to automate your use of self-hosted runners.
|
||||||
|
See the docs [here](../docs/automate.md).
|
||||||
147
scripts/create-latest-svc.sh
Executable file
147
scripts/create-latest-svc.sh
Executable file
@@ -0,0 +1,147 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#
|
||||||
|
# Downloads latest releases (not pre-release) runner
|
||||||
|
# Configures as a service
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# RUNNER_CFG_PAT=<yourPAT> ./create-latest-svc.sh myuser/myrepo my.ghe.deployment.net
|
||||||
|
# RUNNER_CFG_PAT=<yourPAT> ./create-latest-svc.sh myorg my.ghe.deployment.net
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# export RUNNER_CFG_PAT=<yourPAT>
|
||||||
|
# ./create-latest-svc scope [ghe_domain] [name] [user]
|
||||||
|
#
|
||||||
|
# scope required repo (:owner/:repo) or org (:organization)
|
||||||
|
# ghe_domain optional the fully qualified domain name of your GitHub Enterprise Server deployment
|
||||||
|
# name optional defaults to hostname
|
||||||
|
# user optional user svc will run as. defaults to current
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# PATS over envvars are more secure
|
||||||
|
# Should be used on VMs and not containers
|
||||||
|
# Works on OSX and Linux
|
||||||
|
# Assumes x64 arch
|
||||||
|
#
|
||||||
|
|
||||||
|
runner_scope=${1}
|
||||||
|
ghe_hostname=${2}
|
||||||
|
runner_name=${3:-$(hostname)}
|
||||||
|
svc_user=${4:-$USER}
|
||||||
|
|
||||||
|
echo "Configuring runner @ ${runner_scope}"
|
||||||
|
sudo echo
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
# Validate Environment
|
||||||
|
#---------------------------------------
|
||||||
|
runner_plat=linux
|
||||||
|
[ ! -z "$(which sw_vers)" ] && runner_plat=osx;
|
||||||
|
|
||||||
|
function fatal()
|
||||||
|
{
|
||||||
|
echo "error: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi
|
||||||
|
if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi
|
||||||
|
|
||||||
|
which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc"
|
||||||
|
which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc"
|
||||||
|
|
||||||
|
# bail early if there's already a runner there. also sudo early
|
||||||
|
if [ -d ./runner ]; then
|
||||||
|
fatal "Runner already exists. Use a different directory or delete ./runner"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo -u ${svc_user} mkdir runner
|
||||||
|
|
||||||
|
# TODO: validate not in a container
|
||||||
|
# TODO: validate systemd or osx svc installer
|
||||||
|
|
||||||
|
#--------------------------------------
|
||||||
|
# Get a config token
|
||||||
|
#--------------------------------------
|
||||||
|
echo
|
||||||
|
echo "Generating a registration token..."
|
||||||
|
|
||||||
|
base_api_url="https://api.github.com"
|
||||||
|
if [ -n "${ghe_hostname}" ]; then
|
||||||
|
base_api_url="https://${ghe_hostname}/api/v3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# if the scope has a slash, it's a repo runner
|
||||||
|
orgs_or_repos="orgs"
|
||||||
|
if [[ "$runner_scope" == *\/* ]]; then
|
||||||
|
orgs_or_repos="repos"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export RUNNER_TOKEN=$(curl -s -X POST ${base_api_url}/${orgs_or_repos}/${runner_scope}/actions/runners/registration-token -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" | jq -r '.token')
|
||||||
|
|
||||||
|
if [ -z "$RUNNER_TOKEN" ]; then fatal "Failed to get a token"; fi
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
# Download latest released and extract
|
||||||
|
#---------------------------------------
|
||||||
|
echo
|
||||||
|
echo "Downloading latest runner ..."
|
||||||
|
|
||||||
|
# For the GHES Alpha, download the runner from github.com
|
||||||
|
latest_version_label=$(curl -s -X GET 'https://api.github.com/repos/actions/runner/releases/latest' | jq -r '.tag_name')
|
||||||
|
latest_version=$(echo ${latest_version_label:1})
|
||||||
|
runner_file="actions-runner-${runner_plat}-x64-${latest_version}.tar.gz"
|
||||||
|
|
||||||
|
if [ -f "${runner_file}" ]; then
|
||||||
|
echo "${runner_file} exists. skipping download."
|
||||||
|
else
|
||||||
|
runner_url="https://github.com/actions/runner/releases/download/${latest_version_label}/${runner_file}"
|
||||||
|
|
||||||
|
echo "Downloading ${latest_version_label} for ${runner_plat} ..."
|
||||||
|
echo $runner_url
|
||||||
|
|
||||||
|
curl -O -L ${runner_url}
|
||||||
|
fi
|
||||||
|
|
||||||
|
ls -la *.tar.gz
|
||||||
|
|
||||||
|
#---------------------------------------------------
|
||||||
|
# extract to runner directory in this directory
|
||||||
|
#---------------------------------------------------
|
||||||
|
echo
|
||||||
|
echo "Extracting ${runner_file} to ./runner"
|
||||||
|
|
||||||
|
tar xzf "./${runner_file}" -C runner
|
||||||
|
|
||||||
|
# export of pass
|
||||||
|
sudo chown -R $svc_user ./runner
|
||||||
|
|
||||||
|
pushd ./runner
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
# Unattend config
|
||||||
|
#---------------------------------------
|
||||||
|
runner_url="https://github.com/${runner_scope}"
|
||||||
|
if [ -n "${ghe_hostname}" ]; then
|
||||||
|
runner_url="https://${ghe_hostname}/${runner_scope}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Configuring ${runner_name} @ $runner_url"
|
||||||
|
echo "./config.sh --unattended --url $runner_url --token *** --name $runner_name"
|
||||||
|
sudo -E -u ${svc_user} ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
# Configuring as a service
|
||||||
|
#---------------------------------------
|
||||||
|
echo
|
||||||
|
echo "Configuring as a service ..."
|
||||||
|
prefix=""
|
||||||
|
if [ "${runner_plat}" == "linux" ]; then
|
||||||
|
prefix="sudo "
|
||||||
|
fi
|
||||||
|
|
||||||
|
${prefix}./svc.sh install ${svc_user}
|
||||||
|
${prefix}./svc.sh start
|
||||||
83
scripts/delete.sh
Executable file
83
scripts/delete.sh
Executable file
@@ -0,0 +1,83 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#
|
||||||
|
# Force deletes a runner from the service
|
||||||
|
# The caller should have already ensured the runner is gone and/or stopped
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# RUNNER_CFG_PAT=<yourPAT> ./delete.sh myuser/myrepo myname
|
||||||
|
# RUNNER_CFG_PAT=<yourPAT> ./delete.sh myorg
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# export RUNNER_CFG_PAT=<yourPAT>
|
||||||
|
# ./delete.sh scope name
|
||||||
|
#
|
||||||
|
# scope required repo (:owner/:repo) or org (:organization)
|
||||||
|
# name optional defaults to hostname. name to delete
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# PATS over envvars are more secure
|
||||||
|
# Works on OSX and Linux
|
||||||
|
# Assumes x64 arch
|
||||||
|
#
|
||||||
|
|
||||||
|
runner_scope=${1}
|
||||||
|
runner_name=${2}
|
||||||
|
|
||||||
|
echo "Deleting runner ${runner_name} @ ${runner_scope}"
|
||||||
|
|
||||||
|
function fatal()
|
||||||
|
{
|
||||||
|
echo "error: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi
|
||||||
|
if [ -z "${runner_name}" ]; then fatal "supply name as argument 2"; fi
|
||||||
|
if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi
|
||||||
|
|
||||||
|
which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc"
|
||||||
|
which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc"
|
||||||
|
|
||||||
|
base_api_url="https://api.github.com/orgs"
|
||||||
|
if [[ "$runner_scope" == *\/* ]]; then
|
||||||
|
base_api_url="https://api.github.com/repos"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
#--------------------------------------
|
||||||
|
# Ensure offline
|
||||||
|
#--------------------------------------
|
||||||
|
runner_status=$(curl -s -X GET ${base_api_url}/${runner_scope}/actions/runners?per_page=100 -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" \
|
||||||
|
| jq -M -j ".runners | .[] | [select(.name == \"${runner_name}\")] | .[0].status")
|
||||||
|
|
||||||
|
if [ -z "${runner_status}" ]; then
|
||||||
|
fatal "Could not find runner with name ${runner_name}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Status: ${runner_status}"
|
||||||
|
|
||||||
|
if [ "${runner_status}" != "offline" ]; then
|
||||||
|
fatal "Runner should be offline before removing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#--------------------------------------
|
||||||
|
# Get id of runner to remove
|
||||||
|
#--------------------------------------
|
||||||
|
runner_id=$(curl -s -X GET ${base_api_url}/${runner_scope}/actions/runners?per_page=100 -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" \
|
||||||
|
| jq -M -j ".runners | .[] | [select(.name == \"${runner_name}\")] | .[0].id")
|
||||||
|
|
||||||
|
if [ -z "${runner_id}" ]; then
|
||||||
|
fatal "Could not find runner with name ${runner_name}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Removing id ${runner_id}"
|
||||||
|
|
||||||
|
#--------------------------------------
|
||||||
|
# Remove the runner
|
||||||
|
#--------------------------------------
|
||||||
|
curl -s -X DELETE ${base_api_url}/${runner_scope}/actions/runners/${runner_id} -H "authorization: token ${RUNNER_CFG_PAT}"
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
76
scripts/remove-svc.sh
Executable file
76
scripts/remove-svc.sh
Executable file
@@ -0,0 +1,76 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
#
|
||||||
|
# Removes a runner running as a service
|
||||||
|
# Must be run on the machine where the service is run
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# RUNNER_CFG_PAT=<yourPAT> ./remove-svc.sh myuser/myrepo
|
||||||
|
# RUNNER_CFG_PAT=<yourPAT> ./remove-svc.sh myorg
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# export RUNNER_CFG_PAT=<yourPAT>
|
||||||
|
# ./remove-svc scope name
|
||||||
|
#
|
||||||
|
# scope required repo (:owner/:repo) or org (:organization)
|
||||||
|
# name optional defaults to hostname. name to uninstall and remove
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# PATS over envvars are more secure
|
||||||
|
# Should be used on VMs and not containers
|
||||||
|
# Works on OSX and Linux
|
||||||
|
# Assumes x64 arch
|
||||||
|
#
|
||||||
|
|
||||||
|
runner_scope=${1}
|
||||||
|
runner_name=${2:-$(hostname)}
|
||||||
|
|
||||||
|
echo "Uninstalling runner ${runner_name} @ ${runner_scope}"
|
||||||
|
sudo echo
|
||||||
|
|
||||||
|
function fatal()
|
||||||
|
{
|
||||||
|
echo "error: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi
|
||||||
|
if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi
|
||||||
|
|
||||||
|
which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc"
|
||||||
|
which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc"
|
||||||
|
|
||||||
|
runner_plat=linux
|
||||||
|
[ ! -z "$(which sw_vers)" ] && runner_plat=osx;
|
||||||
|
|
||||||
|
#--------------------------------------
|
||||||
|
# Get a remove token
|
||||||
|
#--------------------------------------
|
||||||
|
echo
|
||||||
|
echo "Generating a removal token..."
|
||||||
|
|
||||||
|
# if the scope has a slash, it's an repo runner
|
||||||
|
base_api_url="https://api.github.com/orgs"
|
||||||
|
if [[ "$runner_scope" == *\/* ]]; then
|
||||||
|
base_api_url="https://api.github.com/repos"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export REMOVE_TOKEN=$(curl -s -X POST ${base_api_url}/${runner_scope}/actions/runners/remove-token -H "accept: application/vnd.github.everest-preview+json" -H "authorization: token ${RUNNER_CFG_PAT}" | jq -r '.token')
|
||||||
|
|
||||||
|
if [ -z "$REMOVE_TOKEN" ]; then fatal "Failed to get a token"; fi
|
||||||
|
|
||||||
|
#---------------------------------------
|
||||||
|
# Stop and uninstall the service
|
||||||
|
#---------------------------------------
|
||||||
|
echo
|
||||||
|
echo "Uninstall the service ..."
|
||||||
|
pushd ./runner
|
||||||
|
prefix=""
|
||||||
|
if [ "${runner_plat}" == "linux" ]; then
|
||||||
|
prefix="sudo "
|
||||||
|
fi
|
||||||
|
${prefix}./svc.sh stop
|
||||||
|
${prefix}./svc.sh uninstall
|
||||||
|
${prefix}./config.sh remove --token $REMOVE_TOKEN
|
||||||
@@ -9,7 +9,7 @@ fi
|
|||||||
|
|
||||||
# Determine OS type
|
# Determine OS type
|
||||||
# Debian based OS (Debian, Ubuntu, Linux Mint) has /etc/debian_version
|
# Debian based OS (Debian, Ubuntu, Linux Mint) has /etc/debian_version
|
||||||
# Fedora based OS (Fedora, Redhat, Centos, Oracle Linux 7) has /etc/redhat-release
|
# Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7) has /etc/redhat-release
|
||||||
# SUSE based OS (OpenSUSE, SUSE Enterprise) has ID_LIKE=suse in /etc/os-release
|
# SUSE based OS (OpenSUSE, SUSE Enterprise) has ID_LIKE=suse in /etc/os-release
|
||||||
|
|
||||||
function print_errormessage()
|
function print_errormessage()
|
||||||
@@ -116,12 +116,12 @@ then
|
|||||||
elif [ -e /etc/redhat-release ]
|
elif [ -e /etc/redhat-release ]
|
||||||
then
|
then
|
||||||
echo "The current OS is Fedora based"
|
echo "The current OS is Fedora based"
|
||||||
echo "--------Redhat Version--------"
|
echo "--Fedora/RHEL/CentOS Version--"
|
||||||
cat /etc/redhat-release
|
cat /etc/redhat-release
|
||||||
echo "------------------------------"
|
echo "------------------------------"
|
||||||
|
|
||||||
# use dnf on fedora
|
# use dnf on fedora
|
||||||
# use yum on centos and redhat
|
# use yum on centos and rhel
|
||||||
if [ -e /etc/fedora-release ]
|
if [ -e /etc/fedora-release ]
|
||||||
then
|
then
|
||||||
command -v dnf
|
command -v dnf
|
||||||
@@ -191,7 +191,7 @@ then
|
|||||||
redhatRelease=$(</etc/redhat-release)
|
redhatRelease=$(</etc/redhat-release)
|
||||||
if [[ $redhatRelease == "CentOS release 6."* || $redhatRelease == "Red Hat Enterprise Linux Server release 6."* ]]
|
if [[ $redhatRelease == "CentOS release 6."* || $redhatRelease == "Red Hat Enterprise Linux Server release 6."* ]]
|
||||||
then
|
then
|
||||||
echo "The current OS is Red Hat Enterprise Linux 6 or Centos 6"
|
echo "The current OS is Red Hat Enterprise Linux 6 or CentOS 6"
|
||||||
|
|
||||||
# Install known dependencies, as a best effort.
|
# Install known dependencies, as a best effort.
|
||||||
# The remaining dependencies are covered by the GitHub doc that will be shown by `print_rhel6message`
|
# The remaining dependencies are covered by the GitHub doc that will be shown by `print_rhel6message`
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ namespace GitHub.Runner.Common
|
|||||||
public const int RunnerUpdating = 3;
|
public const int RunnerUpdating = 3;
|
||||||
public const int RunOnceRunnerUpdating = 4;
|
public const int RunOnceRunnerUpdating = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
||||||
|
public static readonly string WorkerCrash = "WORKER_CRASH";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class RunnerEvent
|
public static class RunnerEvent
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
using GitHub.Runner.Common.Util;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.Tracing;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Loader;
|
using System.Runtime.Loader;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Diagnostics.Tracing;
|
|
||||||
using GitHub.DistributedTask.Logging;
|
using GitHub.DistributedTask.Logging;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
namespace GitHub.Runner.Common
|
||||||
@@ -89,6 +88,7 @@ namespace GitHub.Runner.Common
|
|||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
|
this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
|
this.SecretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.XmlDataEscape);
|
this.SecretMasker.AddValueEncoder(ValueEncoders.XmlDataEscape);
|
||||||
|
this.SecretMasker.AddValueEncoder(ValueEncoders.TrimDoubleQuotes);
|
||||||
|
|
||||||
// Create the trace manager.
|
// Create the trace manager.
|
||||||
if (string.IsNullOrEmpty(logFile))
|
if (string.IsNullOrEmpty(logFile))
|
||||||
@@ -614,9 +614,8 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
public static HttpClientHandler CreateHttpClientHandler(this IHostContext context)
|
public static HttpClientHandler CreateHttpClientHandler(this IHostContext context)
|
||||||
{
|
{
|
||||||
HttpClientHandler clientHandler = new HttpClientHandler();
|
var handlerFactory = context.GetService<IHttpClientHandlerFactory>();
|
||||||
clientHandler.Proxy = context.WebProxy;
|
return handlerFactory.CreateClientHandler(context.WebProxy);
|
||||||
return clientHandler;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
src/Runner.Common/HttpClientHandlerFactory.cs
Normal file
19
src/Runner.Common/HttpClientHandlerFactory.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Common
|
||||||
|
{
|
||||||
|
[ServiceLocator(Default = typeof(HttpClientHandlerFactory))]
|
||||||
|
public interface IHttpClientHandlerFactory : IRunnerService
|
||||||
|
{
|
||||||
|
HttpClientHandler CreateClientHandler(RunnerWebProxy webProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HttpClientHandlerFactory : RunnerService, IHttpClientHandlerFactory
|
||||||
|
{
|
||||||
|
public HttpClientHandler CreateClientHandler(RunnerWebProxy webProxy)
|
||||||
|
{
|
||||||
|
return new HttpClientHandler() { Proxy = webProxy };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -858,7 +858,6 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We need send detailInfo back to DT in order to add an issue for the job
|
|
||||||
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, Guid lockToken, TaskResult result, string detailInfo = null)
|
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, Guid lockToken, TaskResult result, string detailInfo = null)
|
||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
@@ -952,8 +951,10 @@ namespace GitHub.Runner.Listener
|
|||||||
ArgUtil.NotNull(timeline, nameof(timeline));
|
ArgUtil.NotNull(timeline, nameof(timeline));
|
||||||
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
||||||
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
||||||
|
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = errorMessage };
|
||||||
|
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
||||||
jobRecord.ErrorCount++;
|
jobRecord.ErrorCount++;
|
||||||
jobRecord.Issues.Add(new Issue() { Type = IssueType.Error, Message = errorMessage });
|
jobRecord.Issues.Add(unhandledExceptionIssue);
|
||||||
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
|
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -150,6 +150,20 @@ namespace GitHub.Runner.Listener
|
|||||||
Trace.Error("Catch exception during create session.");
|
Trace.Error("Catch exception during create session.");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
|
|
||||||
|
if (ex is VssOAuthTokenRequestException && creds.Federated is VssOAuthCredential vssOAuthCred)
|
||||||
|
{
|
||||||
|
// Check whether we get 401 because the runner registration already removed by the service.
|
||||||
|
// If the runner registration get deleted, we can't exchange oauth token.
|
||||||
|
Trace.Error("Test oauth app registration.");
|
||||||
|
var oauthTokenProvider = new VssOAuthTokenProvider(vssOAuthCred, new Uri(serverUrl));
|
||||||
|
var authError = await oauthTokenProvider.ValidateCredentialAsync(token);
|
||||||
|
if (string.Equals(authError, "invalid_client", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_term.WriteError("Failed to create a session. The runner registration has been deleted from the server, please re-configure.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsSessionCreationExceptionRetriable(ex))
|
if (!IsSessionCreationExceptionRetriable(ex))
|
||||||
{
|
{
|
||||||
if (_useMigratedCredentials)
|
if (_useMigratedCredentials)
|
||||||
|
|||||||
@@ -486,7 +486,10 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
foreach (var property in command.Properties)
|
foreach (var property in command.Properties)
|
||||||
{
|
{
|
||||||
issue.Data[property.Key] = property.Value;
|
if (!string.Equals(property.Key, Constants.Runner.InternalTelemetryIssueDataKey, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
issue.Data[property.Key] = property.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.AddIssue(issue);
|
context.AddIssue(issue);
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.Runner.Worker.Container;
|
||||||
using GitHub.Services.Common;
|
using GitHub.Services.Common;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants;
|
using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants;
|
||||||
|
|
||||||
@@ -72,14 +70,8 @@ namespace GitHub.Runner.Worker
|
|||||||
executionContext.Warning("The 'PREVIEW_ACTION_TOKEN' secret is deprecated. Please remove it from the repository's secrets");
|
executionContext.Warning("The 'PREVIEW_ACTION_TOKEN' secret is deprecated. Please remove it from the repository's secrets");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the cache (for self-hosted runners)
|
// // Clear the cache (for self-hosted runners)
|
||||||
// Note, temporarily avoid this step for the on-premises product, to avoid rate limiting.
|
// IOUtil.DeleteDirectory(HostContext.GetDirectory(WellKnownDirectory.Actions), executionContext.CancellationToken);
|
||||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
|
||||||
var isHostedServer = configurationStore.GetSettings().IsHostedServer;
|
|
||||||
if (isHostedServer)
|
|
||||||
{
|
|
||||||
IOUtil.DeleteDirectory(HostContext.GetDirectory(WellKnownDirectory.Actions), executionContext.CancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var action in actions)
|
foreach (var action in actions)
|
||||||
{
|
{
|
||||||
@@ -490,7 +482,7 @@ namespace GitHub.Runner.Worker
|
|||||||
ArgUtil.NotNullOrEmpty(repositoryReference.Ref, nameof(repositoryReference.Ref));
|
ArgUtil.NotNullOrEmpty(repositoryReference.Ref, nameof(repositoryReference.Ref));
|
||||||
|
|
||||||
string destDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repositoryReference.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repositoryReference.Ref);
|
string destDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repositoryReference.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repositoryReference.Ref);
|
||||||
string watermarkFile = destDirectory + ".completed";
|
string watermarkFile = GetWatermarkFilePath(destDirectory);
|
||||||
if (File.Exists(watermarkFile))
|
if (File.Exists(watermarkFile))
|
||||||
{
|
{
|
||||||
executionContext.Debug($"Action '{repositoryReference.Name}@{repositoryReference.Ref}' already downloaded at '{destDirectory}'.");
|
executionContext.Debug($"Action '{repositoryReference.Name}@{repositoryReference.Ref}' already downloaded at '{destDirectory}'.");
|
||||||
@@ -504,27 +496,84 @@ namespace GitHub.Runner.Worker
|
|||||||
executionContext.Output($"Download action repository '{repositoryReference.Name}@{repositoryReference.Ref}'");
|
executionContext.Output($"Download action repository '{repositoryReference.Name}@{repositoryReference.Ref}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
#if OS_WINDOWS
|
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
||||||
string archiveLink = $"https://api.github.com/repos/{repositoryReference.Name}/zipball/{repositoryReference.Ref}";
|
var isHostedServer = configurationStore.GetSettings().IsHostedServer;
|
||||||
#else
|
if (isHostedServer)
|
||||||
string archiveLink = $"https://api.github.com/repos/{repositoryReference.Name}/tarball/{repositoryReference.Ref}";
|
{
|
||||||
#endif
|
string apiUrl = GetApiUrl(executionContext);
|
||||||
Trace.Info($"Download archive '{archiveLink}' to '{destDirectory}'.");
|
string archiveLink = BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref);
|
||||||
|
Trace.Info($"Download archive '{archiveLink}' to '{destDirectory}'.");
|
||||||
|
await DownloadRepositoryActionAsync(executionContext, archiveLink, destDirectory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string apiUrl = GetApiUrl(executionContext);
|
||||||
|
|
||||||
|
// URLs to try:
|
||||||
|
var archiveLinks = new List<string> {
|
||||||
|
// A built-in action or an action the user has created, on their GHES instance
|
||||||
|
// Example: https://my-ghes/api/v3/repos/my-org/my-action/tarball/v1
|
||||||
|
BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref),
|
||||||
|
|
||||||
|
// // A community action, synced to their GHES instance
|
||||||
|
// // Example: https://my-ghes/api/v3/repos/actions-community/some-org-some-action/tarball/v1
|
||||||
|
// BuildLinkToActionArchive(apiUrl, $"actions-community/{repositoryReference.Name.Replace("/", "-")}", repositoryReference.Ref)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var archiveLink in archiveLinks)
|
||||||
|
{
|
||||||
|
Trace.Info($"Download archive '{archiveLink}' to '{destDirectory}'.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DownloadRepositoryActionAsync(executionContext, archiveLink, destDirectory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (ActionNotFoundException)
|
||||||
|
{
|
||||||
|
Trace.Info($"Failed to find the action '{repositoryReference.Name}' at ref '{repositoryReference.Ref}' at {archiveLink}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new ActionNotFoundException($"Failed to find the action '{repositoryReference.Name}' at ref '{repositoryReference.Ref}'. Paths attempted: {string.Join(", ", archiveLinks)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetApiUrl(IExecutionContext executionContext)
|
||||||
|
{
|
||||||
|
string apiUrl = executionContext.GetGitHubContext("api_url");
|
||||||
|
if (!string.IsNullOrEmpty(apiUrl))
|
||||||
|
{
|
||||||
|
return apiUrl;
|
||||||
|
}
|
||||||
|
// Once the api_url is set for hosted, we can remove this fallback (it doesn't make sense for GHES)
|
||||||
|
return "https://api.github.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildLinkToActionArchive(string apiUrl, string repository, string @ref)
|
||||||
|
{
|
||||||
|
#if OS_WINDOWS
|
||||||
|
return $"https://github.com/{repository}/zipball/{@ref}";
|
||||||
|
#else
|
||||||
|
return $"https://github.com/{repository}/tarball/{@ref}";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, string link, string destDirectory)
|
||||||
|
{
|
||||||
//download and extract action in a temp folder and rename it on success
|
//download and extract action in a temp folder and rename it on success
|
||||||
string tempDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), "_temp_" + Guid.NewGuid());
|
string tempDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), "_temp_" + Guid.NewGuid());
|
||||||
Directory.CreateDirectory(tempDirectory);
|
Directory.CreateDirectory(tempDirectory);
|
||||||
|
|
||||||
|
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.zip");
|
string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.zip");
|
||||||
#else
|
#else
|
||||||
string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.tar.gz");
|
string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.tar.gz");
|
||||||
#endif
|
#endif
|
||||||
Trace.Info($"Save archive '{archiveLink}' into {archiveFile}.");
|
|
||||||
|
Trace.Info($"Save archive '{link}' into {archiveFile}.");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
|
|
||||||
// Allow up to 20 * 60s for any action to be downloaded from github graph.
|
// Allow up to 20 * 60s for any action to be downloaded from github graph.
|
||||||
@@ -541,64 +590,76 @@ namespace GitHub.Runner.Worker
|
|||||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||||
using (var httpClient = new HttpClient(httpClientHandler))
|
using (var httpClient = new HttpClient(httpClientHandler))
|
||||||
{
|
{
|
||||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
// var authToken = Environment.GetEnvironmentVariable("_GITHUB_ACTION_TOKEN");
|
||||||
var isHostedServer = configurationStore.GetSettings().IsHostedServer;
|
// if (string.IsNullOrEmpty(authToken))
|
||||||
if (isHostedServer)
|
// {
|
||||||
{
|
// // TODO: Deprecate the PREVIEW_ACTION_TOKEN
|
||||||
var authToken = Environment.GetEnvironmentVariable("_GITHUB_ACTION_TOKEN");
|
// authToken = executionContext.Variables.Get("PREVIEW_ACTION_TOKEN");
|
||||||
if (string.IsNullOrEmpty(authToken))
|
// }
|
||||||
{
|
|
||||||
// TODO: Deprecate the PREVIEW_ACTION_TOKEN
|
|
||||||
authToken = executionContext.Variables.Get("PREVIEW_ACTION_TOKEN");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(authToken))
|
// if (!string.IsNullOrEmpty(authToken))
|
||||||
|
// {
|
||||||
|
// HostContext.SecretMasker.AddValue(authToken);
|
||||||
|
// var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"PAT:{authToken}"));
|
||||||
|
// httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodingToken);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// var accessToken = executionContext.GetGitHubContext("token");
|
||||||
|
// var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{accessToken}"));
|
||||||
|
// httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodingToken);
|
||||||
|
// }
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||||
|
using (var response = await httpClient.GetAsync(link))
|
||||||
|
{
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
HostContext.SecretMasker.AddValue(authToken);
|
using (var result = await response.Content.ReadAsStreamAsync())
|
||||||
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"PAT:{authToken}"));
|
{
|
||||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodingToken);
|
await result.CopyToAsync(fs, _defaultCopyBufferSize, actionDownloadCancellation.Token);
|
||||||
|
await fs.FlushAsync(actionDownloadCancellation.Token);
|
||||||
|
|
||||||
|
// download succeed, break out the retry loop.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
// It doesn't make sense to retry in this case, so just stop
|
||||||
|
throw new ActionNotFoundException(new Uri(link));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var accessToken = executionContext.GetGitHubContext("token");
|
// Something else bad happened, let's go to our retry logic
|
||||||
var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{accessToken}"));
|
response.EnsureSuccessStatusCode();
|
||||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodingToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// Intentionally empty. Temporary for GHES alpha release, download from dotcom unauthenticated.
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
|
||||||
using (var result = await httpClient.GetStreamAsync(archiveLink))
|
|
||||||
{
|
|
||||||
await result.CopyToAsync(fs, _defaultCopyBufferSize, actionDownloadCancellation.Token);
|
|
||||||
await fs.FlushAsync(actionDownloadCancellation.Token);
|
|
||||||
|
|
||||||
// download succeed, break out the retry loop.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (executionContext.CancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (executionContext.CancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
Trace.Info($"Action download has been cancelled.");
|
Trace.Info("Action download has been cancelled.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (ActionNotFoundException)
|
||||||
|
{
|
||||||
|
Trace.Info($"The action at '{link}' does not exist");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (retryCount < 2)
|
catch (Exception ex) when (retryCount < 2)
|
||||||
{
|
{
|
||||||
retryCount++;
|
retryCount++;
|
||||||
Trace.Error($"Fail to download archive '{archiveLink}' -- Attempt: {retryCount}");
|
Trace.Error($"Fail to download archive '{link}' -- Attempt: {retryCount}");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
if (actionDownloadTimeout.Token.IsCancellationRequested)
|
if (actionDownloadTimeout.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// action download didn't finish within timeout
|
// action download didn't finish within timeout
|
||||||
executionContext.Warning($"Action '{archiveLink}' didn't finish download within {timeoutSeconds} seconds.");
|
executionContext.Warning($"Action '{link}' didn't finish download within {timeoutSeconds} seconds.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
executionContext.Warning($"Failed to download action '{archiveLink}'. Error {ex.Message}");
|
executionContext.Warning($"Failed to download action '{link}'. Error: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -612,7 +673,7 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArgUtil.NotNullOrEmpty(archiveFile, nameof(archiveFile));
|
ArgUtil.NotNullOrEmpty(archiveFile, nameof(archiveFile));
|
||||||
executionContext.Debug($"Download '{archiveLink}' to '{archiveFile}'");
|
executionContext.Debug($"Download '{link}' to '{archiveFile}'");
|
||||||
|
|
||||||
var stagingDirectory = Path.Combine(tempDirectory, "_staging");
|
var stagingDirectory = Path.Combine(tempDirectory, "_staging");
|
||||||
Directory.CreateDirectory(stagingDirectory);
|
Directory.CreateDirectory(stagingDirectory);
|
||||||
@@ -662,6 +723,7 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
|
|
||||||
Trace.Verbose("Create watermark file indicate action download succeed.");
|
Trace.Verbose("Create watermark file indicate action download succeed.");
|
||||||
|
string watermarkFile = GetWatermarkFilePath(destDirectory);
|
||||||
File.WriteAllText(watermarkFile, DateTime.UtcNow.ToString());
|
File.WriteAllText(watermarkFile, DateTime.UtcNow.ToString());
|
||||||
|
|
||||||
executionContext.Debug($"Archive '{archiveFile}' has been unzipped into '{destDirectory}'.");
|
executionContext.Debug($"Archive '{archiveFile}' has been unzipped into '{destDirectory}'.");
|
||||||
@@ -686,6 +748,8 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetWatermarkFilePath(string directory) => directory + ".completed";
|
||||||
|
|
||||||
private ActionContainer PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction)
|
private ActionContainer PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction)
|
||||||
{
|
{
|
||||||
var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference;
|
var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference;
|
||||||
@@ -931,4 +995,3 @@ namespace GitHub.Runner.Worker
|
|||||||
public string ActionRepository { get; set; }
|
public string ActionRepository { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
src/Runner.Worker/ActionNotFoundException.cs
Normal file
33
src/Runner.Worker/ActionNotFoundException.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Worker
|
||||||
|
{
|
||||||
|
public class ActionNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public ActionNotFoundException(Uri actionUri)
|
||||||
|
: base(FormatMessage(actionUri))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionNotFoundException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionNotFoundException(string message, System.Exception inner)
|
||||||
|
: base(message, inner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ActionNotFoundException(SerializationInfo info, StreamingContext context)
|
||||||
|
: base(info, context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatMessage(Uri actionUri)
|
||||||
|
{
|
||||||
|
return $"An action could not be found at the URI '{actionUri}'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,8 +143,10 @@ namespace GitHub.Runner.Worker
|
|||||||
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
|
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
|
||||||
var inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
|
var inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
|
||||||
|
|
||||||
|
var userInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (KeyValuePair<string, string> input in inputs)
|
foreach (KeyValuePair<string, string> input in inputs)
|
||||||
{
|
{
|
||||||
|
userInputs.Add(input.Key);
|
||||||
string message = "";
|
string message = "";
|
||||||
if (definition.Data?.Deprecated?.TryGetValue(input.Key, out message) == true)
|
if (definition.Data?.Deprecated?.TryGetValue(input.Key, out message) == true)
|
||||||
{
|
{
|
||||||
@@ -152,13 +154,15 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var validInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
// Merge the default inputs from the definition
|
// Merge the default inputs from the definition
|
||||||
if (definition.Data?.Inputs != null)
|
if (definition.Data?.Inputs != null)
|
||||||
{
|
{
|
||||||
var manifestManager = HostContext.GetService<IActionManifestManager>();
|
var manifestManager = HostContext.GetService<IActionManifestManager>();
|
||||||
foreach (var input in (definition.Data?.Inputs))
|
foreach (var input in definition.Data.Inputs)
|
||||||
{
|
{
|
||||||
string key = input.Key.AssertString("action input name").Value;
|
string key = input.Key.AssertString("action input name").Value;
|
||||||
|
validInputs.Add(key);
|
||||||
if (!inputs.ContainsKey(key))
|
if (!inputs.ContainsKey(key))
|
||||||
{
|
{
|
||||||
inputs[key] = manifestManager.EvaluateDefaultInput(ExecutionContext, key, input.Value);
|
inputs[key] = manifestManager.EvaluateDefaultInput(ExecutionContext, key, input.Value);
|
||||||
@@ -166,6 +170,14 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var input in userInputs)
|
||||||
|
{
|
||||||
|
if (!validInputs.Contains(input))
|
||||||
|
{
|
||||||
|
ExecutionContext.Warning($"Unexpected input '{input}', valid inputs are ['{string.Join("', '", validInputs)}']");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load the action environment.
|
// Load the action environment.
|
||||||
ExecutionContext.Debug("Loading env");
|
ExecutionContext.Debug("Loading env");
|
||||||
var environment = new Dictionary<String, String>(VarUtil.EnvironmentVariableKeyComparer);
|
var environment = new Dictionary<String, String>(VarUtil.EnvironmentVariableKeyComparer);
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
"action",
|
"action",
|
||||||
"actor",
|
"actor",
|
||||||
"api_url", // temp for GHES alpha release
|
"api_url",
|
||||||
"base_ref",
|
"base_ref",
|
||||||
"event_name",
|
"event_name",
|
||||||
"event_path",
|
"event_path",
|
||||||
|
"graphql_url",
|
||||||
"head_ref",
|
"head_ref",
|
||||||
"job",
|
"job",
|
||||||
"ref",
|
"ref",
|
||||||
@@ -22,7 +23,7 @@ namespace GitHub.Runner.Worker
|
|||||||
"run_id",
|
"run_id",
|
||||||
"run_number",
|
"run_number",
|
||||||
"sha",
|
"sha",
|
||||||
"url", // temp for GHES alpha release
|
"url",
|
||||||
"workflow",
|
"workflow",
|
||||||
"workspace",
|
"workspace",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -149,14 +149,14 @@ namespace GitHub.Runner.Worker.Handlers
|
|||||||
throw new NotSupportedException(msg);
|
throw new NotSupportedException(msg);
|
||||||
}
|
}
|
||||||
nodeExternal = "node12_alpine";
|
nodeExternal = "node12_alpine";
|
||||||
executionContext.Output($"Container distribution is alpine. Running JavaScript Action with external tool: {nodeExternal}");
|
executionContext.Debug($"Container distribution is alpine. Running JavaScript Action with external tool: {nodeExternal}");
|
||||||
return nodeExternal;
|
return nodeExternal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Optimistically use the default
|
// Optimistically use the default
|
||||||
nodeExternal = "node12";
|
nodeExternal = "node12";
|
||||||
executionContext.Output($"Running JavaScript Action with default external tool: {nodeExternal}");
|
executionContext.Debug($"Running JavaScript Action with default external tool: {nodeExternal}");
|
||||||
return nodeExternal;
|
return nodeExternal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,12 +131,13 @@ namespace GitHub.Runner.Worker
|
|||||||
// Temporary hack for GHES alpha
|
// Temporary hack for GHES alpha
|
||||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
||||||
var runnerSettings = configurationStore.GetSettings();
|
var runnerSettings = configurationStore.GetSettings();
|
||||||
if (!runnerSettings.IsHostedServer && !string.IsNullOrEmpty(runnerSettings.GitHubUrl))
|
if (string.IsNullOrEmpty(context.GetGitHubContext("url")) && !runnerSettings.IsHostedServer && !string.IsNullOrEmpty(runnerSettings.GitHubUrl))
|
||||||
{
|
{
|
||||||
var url = new Uri(runnerSettings.GitHubUrl);
|
var url = new Uri(runnerSettings.GitHubUrl);
|
||||||
var portInfo = url.IsDefaultPort ? string.Empty : $":{url.Port.ToString(CultureInfo.InvariantCulture)}";
|
var portInfo = url.IsDefaultPort ? string.Empty : $":{url.Port.ToString(CultureInfo.InvariantCulture)}";
|
||||||
context.SetGitHubContext("url", $"{url.Scheme}://{url.Host}{portInfo}");
|
context.SetGitHubContext("url", $"{url.Scheme}://{url.Host}{portInfo}");
|
||||||
context.SetGitHubContext("api_url", $"{url.Scheme}://{url.Host}{portInfo}/api/v3");
|
context.SetGitHubContext("api_url", $"{url.Scheme}://{url.Host}{portInfo}/api/v3");
|
||||||
|
context.SetGitHubContext("graphql_url", $"{url.Scheme}://{url.Host}{portInfo}/api/graphql");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate the job-level environment variables
|
// Evaluate the job-level environment variables
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
return SecurityElement.Escape(value);
|
return SecurityElement.Escape(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String TrimDoubleQuotes(String value)
|
||||||
|
{
|
||||||
|
var trimmed = string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(value) &&
|
||||||
|
value.Length > 8 &&
|
||||||
|
value.StartsWith('"') &&
|
||||||
|
value.EndsWith('"'))
|
||||||
|
{
|
||||||
|
trimmed = value.Substring(1, value.Length - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
private static string Base64StringEscapeShift(String value, int shift)
|
private static string Base64StringEscapeShift(String value, int shift)
|
||||||
{
|
{
|
||||||
var bytes = Encoding.UTF8.GetBytes(value);
|
var bytes = Encoding.UTF8.GetBytes(value);
|
||||||
|
|||||||
@@ -119,6 +119,15 @@ namespace GitHub.Services.OAuth
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> ValidateCredentialAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tokenHttpClient = new VssOAuthTokenHttpClient(this.SignInUrl);
|
||||||
|
var tokenResponse = await tokenHttpClient.GetTokenAsync(this.Grant, this.ClientCredential, this.TokenParameters, cancellationToken);
|
||||||
|
|
||||||
|
// return the underlying authentication error
|
||||||
|
return tokenResponse.Error;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Issues a token request to the configured secure token service. On success, the access token issued by the
|
/// Issues a token request to the configured secure token service. On success, the access token issued by the
|
||||||
/// token service is returned to the caller
|
/// token service is returned to the caller
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
_hc.SecretMasker.AddValue("Pass word 123!");
|
_hc.SecretMasker.AddValue("Pass word 123!");
|
||||||
_hc.SecretMasker.AddValue("Pass<word>123!");
|
_hc.SecretMasker.AddValue("Pass<word>123!");
|
||||||
_hc.SecretMasker.AddValue("Pass'word'123!");
|
_hc.SecretMasker.AddValue("Pass'word'123!");
|
||||||
|
_hc.SecretMasker.AddValue("\"Password123!!\"");
|
||||||
|
_hc.SecretMasker.AddValue("\"short\"");
|
||||||
|
|
||||||
// Assert.
|
// Assert.
|
||||||
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Password123!123"));
|
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Password123!123"));
|
||||||
@@ -99,6 +101,9 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
Assert.Equal("YWJjOlBh***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abc:Password123!"))));
|
Assert.Equal("YWJjOlBh***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abc:Password123!"))));
|
||||||
Assert.Equal("YWJjZDpQ***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcd:Password123!"))));
|
Assert.Equal("YWJjZDpQ***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcd:Password123!"))));
|
||||||
Assert.Equal("YWJjZGU6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcde:Password123!"))));
|
Assert.Equal("YWJjZGU6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcde:Password123!"))));
|
||||||
|
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Password123!!123"));
|
||||||
|
Assert.Equal("123short123", _hc.SecretMasker.MaskSecrets("123short123"));
|
||||||
|
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123\"short\"123"));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
private static readonly List<string> SkippedFiles = new List<string>()
|
private static readonly List<string> SkippedFiles = new List<string>()
|
||||||
{
|
{
|
||||||
"Runner.Common\\HostContext.cs",
|
"Runner.Common\\HostContext.cs",
|
||||||
"Runner.Common/HostContext.cs"
|
"Runner.Common/HostContext.cs",
|
||||||
|
"Runner.Common\\HttpClientHandlerFactory.cs",
|
||||||
|
"Runner.Common/HttpClientHandlerFactory.cs"
|
||||||
};
|
};
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
using GitHub.DistributedTask.Expressions2;
|
using System;
|
||||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
|
||||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
||||||
using GitHub.DistributedTask.WebApi;
|
|
||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using GitHub.Runner.Worker;
|
|
||||||
using GitHub.Runner.Worker.Container;
|
|
||||||
using Moq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Reflection;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using GitHub.DistributedTask.Expressions2;
|
||||||
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using GitHub.Runner.Sdk;
|
||||||
|
using GitHub.Runner.Worker;
|
||||||
|
using GitHub.Runner.Worker.Container;
|
||||||
|
using Moq;
|
||||||
|
using Moq.Protected;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
@@ -114,47 +116,175 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
public async void PrepareActions_SkipDownloadActionFromGraphWhenCached_OnPremises()
|
public async void PrepareActions_DownloadBuiltInActionFromGraph_OnPremises()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
Setup();
|
Setup();
|
||||||
var actionId = Guid.NewGuid();
|
const string ActionName = "actions/sample-action";
|
||||||
var actions = new List<Pipelines.ActionStep>
|
var actions = new List<Pipelines.ActionStep>
|
||||||
{
|
{
|
||||||
new Pipelines.ActionStep()
|
new Pipelines.ActionStep()
|
||||||
{
|
{
|
||||||
Name = "action",
|
Name = "action",
|
||||||
Id = actionId,
|
Id = Guid.NewGuid(),
|
||||||
Reference = new Pipelines.RepositoryPathReference()
|
Reference = new Pipelines.RepositoryPathReference()
|
||||||
{
|
{
|
||||||
Name = "actions/no-such-action",
|
Name = ActionName,
|
||||||
Ref = "master",
|
Ref = "master",
|
||||||
RepositoryType = "GitHub"
|
RepositoryType = "GitHub"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_configurationStore.Object.GetSettings().IsHostedServer = false;
|
|
||||||
var actionDirectory = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions/no-such-action", "master");
|
|
||||||
Directory.CreateDirectory(actionDirectory);
|
|
||||||
var watermarkFile = $"{actionDirectory}.completed";
|
|
||||||
File.WriteAllText(watermarkFile, DateTime.UtcNow.ToString());
|
|
||||||
var actionFile = Path.Combine(actionDirectory, "action.yml");
|
|
||||||
File.WriteAllText(actionFile, @"
|
|
||||||
name: ""no-such-action""
|
|
||||||
runs:
|
|
||||||
using: node12
|
|
||||||
main: no-such-action.js
|
|
||||||
");
|
|
||||||
var testFile = Path.Combine(actionDirectory, "test-file");
|
|
||||||
File.WriteAllText(testFile, "asdf");
|
|
||||||
|
|
||||||
// Act
|
// Return a valid action from GHES via mock
|
||||||
|
const string ApiUrl = "https://ghes.example.com/api/v3";
|
||||||
|
string expectedArchiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master");
|
||||||
|
string archiveFile = await CreateRepoArchive();
|
||||||
|
using var stream = File.OpenRead(archiveFile);
|
||||||
|
var mockClientHandler = new Mock<HttpClientHandler>();
|
||||||
|
mockClientHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(m => m.RequestUri == new Uri(expectedArchiveLink)), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) });
|
||||||
|
|
||||||
|
var mockHandlerFactory = new Mock<IHttpClientHandlerFactory>();
|
||||||
|
mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny<RunnerWebProxy>())).Returns(mockClientHandler.Object);
|
||||||
|
_hc.SetSingleton(mockHandlerFactory.Object);
|
||||||
|
|
||||||
|
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl);
|
||||||
|
_configurationStore.Object.GetSettings().IsHostedServer = false;
|
||||||
|
|
||||||
|
//Act
|
||||||
await _actionManager.PrepareActionsAsync(_ec.Object, actions);
|
await _actionManager.PrepareActionsAsync(_ec.Object, actions);
|
||||||
|
|
||||||
// Assert
|
//Assert
|
||||||
Assert.True(File.Exists(testFile));
|
var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed");
|
||||||
|
Assert.True(File.Exists(watermarkFile));
|
||||||
|
|
||||||
|
var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml");
|
||||||
|
Assert.True(File.Exists(actionYamlFile));
|
||||||
|
_hc.GetTrace().Info(File.ReadAllText(actionYamlFile));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async void PrepareActions_DownloadCommunityActionFromGraph_OnPremises()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Setup();
|
||||||
|
const string ActionName = "ownerName/sample-action";
|
||||||
|
const string MungedActionName = "actions-community/ownerName-sample-action";
|
||||||
|
var actions = new List<Pipelines.ActionStep>
|
||||||
|
{
|
||||||
|
new Pipelines.ActionStep()
|
||||||
|
{
|
||||||
|
Name = "action",
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Reference = new Pipelines.RepositoryPathReference()
|
||||||
|
{
|
||||||
|
Name = ActionName,
|
||||||
|
Ref = "master",
|
||||||
|
RepositoryType = "GitHub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return a valid action from GHES via mock
|
||||||
|
const string ApiUrl = "https://ghes.example.com/api/v3";
|
||||||
|
string builtInArchiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master");
|
||||||
|
string mungedArchiveLink = GetLinkToActionArchive(ApiUrl, MungedActionName, "master");
|
||||||
|
string archiveFile = await CreateRepoArchive();
|
||||||
|
using var stream = File.OpenRead(archiveFile);
|
||||||
|
var mockClientHandler = new Mock<HttpClientHandler>();
|
||||||
|
mockClientHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(m => m.RequestUri == new Uri(builtInArchiveLink)), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||||
|
mockClientHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(m => m.RequestUri == new Uri(mungedArchiveLink)), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) });
|
||||||
|
|
||||||
|
var mockHandlerFactory = new Mock<IHttpClientHandlerFactory>();
|
||||||
|
mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny<RunnerWebProxy>())).Returns(mockClientHandler.Object);
|
||||||
|
_hc.SetSingleton(mockHandlerFactory.Object);
|
||||||
|
|
||||||
|
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl);
|
||||||
|
_configurationStore.Object.GetSettings().IsHostedServer = false;
|
||||||
|
|
||||||
|
//Act
|
||||||
|
await _actionManager.PrepareActionsAsync(_ec.Object, actions);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed");
|
||||||
|
Assert.True(File.Exists(watermarkFile));
|
||||||
|
|
||||||
|
var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml");
|
||||||
|
Assert.True(File.Exists(actionYamlFile));
|
||||||
|
_hc.GetTrace().Info(File.ReadAllText(actionYamlFile));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Teardown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async void PrepareActions_DownloadUnknownActionFromGraph_OnPremises()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
Setup();
|
||||||
|
const string ActionName = "ownerName/sample-action";
|
||||||
|
var actions = new List<Pipelines.ActionStep>
|
||||||
|
{
|
||||||
|
new Pipelines.ActionStep()
|
||||||
|
{
|
||||||
|
Name = "action",
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Reference = new Pipelines.RepositoryPathReference()
|
||||||
|
{
|
||||||
|
Name = ActionName,
|
||||||
|
Ref = "master",
|
||||||
|
RepositoryType = "GitHub"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return a valid action from GHES via mock
|
||||||
|
const string ApiUrl = "https://ghes.example.com/api/v3";
|
||||||
|
string archiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master");
|
||||||
|
string archiveFile = await CreateRepoArchive();
|
||||||
|
using var stream = File.OpenRead(archiveFile);
|
||||||
|
var mockClientHandler = new Mock<HttpClientHandler>();
|
||||||
|
mockClientHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||||
|
|
||||||
|
var mockHandlerFactory = new Mock<IHttpClientHandlerFactory>();
|
||||||
|
mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny<RunnerWebProxy>())).Returns(mockClientHandler.Object);
|
||||||
|
_hc.SetSingleton(mockHandlerFactory.Object);
|
||||||
|
|
||||||
|
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl);
|
||||||
|
_configurationStore.Object.GetSettings().IsHostedServer = false;
|
||||||
|
|
||||||
|
//Act
|
||||||
|
Func<Task> action = async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions);
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
await Assert.ThrowsAsync<ActionNotFoundException>(action);
|
||||||
|
|
||||||
|
var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed");
|
||||||
|
Assert.False(File.Exists(watermarkFile));
|
||||||
|
|
||||||
|
var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml");
|
||||||
|
Assert.False(File.Exists(actionYamlFile));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1737,6 +1867,82 @@ runs:
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a sample action in an archive on disk, similar to the archive
|
||||||
|
/// retrieved from GitHub's or GHES' repository API.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The path on disk to the archive.</returns>
|
||||||
|
#if OS_WINDOWS
|
||||||
|
private Task<string> CreateRepoArchive()
|
||||||
|
#else
|
||||||
|
private async Task<string> CreateRepoArchive()
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
const string Content = @"
|
||||||
|
# Container action
|
||||||
|
name: 'Hello World'
|
||||||
|
description: 'Greet the world'
|
||||||
|
author: 'GitHub'
|
||||||
|
icon: 'hello.svg' # vector art to display in the GitHub Marketplace
|
||||||
|
color: 'green' # optional, decorates the entry in the GitHub Marketplace
|
||||||
|
runs:
|
||||||
|
using: 'node12'
|
||||||
|
main: 'task.js'
|
||||||
|
";
|
||||||
|
CreateAction(yamlContent: Content, instance: out _, directory: out string directory);
|
||||||
|
|
||||||
|
var tempDir = _hc.GetDirectory(WellKnownDirectory.Temp);
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
var archiveFile = Path.Combine(tempDir, Path.GetRandomFileName());
|
||||||
|
var trace = _hc.GetTrace();
|
||||||
|
|
||||||
|
#if OS_WINDOWS
|
||||||
|
ZipFile.CreateFromDirectory(directory, archiveFile, CompressionLevel.Fastest, includeBaseDirectory: true);
|
||||||
|
return Task.FromResult(archiveFile);
|
||||||
|
#else
|
||||||
|
string tar = WhichUtil.Which("tar", require: true, trace: trace);
|
||||||
|
|
||||||
|
// tar -xzf
|
||||||
|
using (var processInvoker = new ProcessInvokerWrapper())
|
||||||
|
{
|
||||||
|
processInvoker.Initialize(_hc);
|
||||||
|
processInvoker.OutputDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(args.Data))
|
||||||
|
{
|
||||||
|
trace.Info(args.Data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
processInvoker.ErrorDataReceived += new EventHandler<ProcessDataReceivedEventArgs>((sender, args) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(args.Data))
|
||||||
|
{
|
||||||
|
trace.Error(args.Data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
string cwd = Path.GetDirectoryName(directory);
|
||||||
|
string inputDirectory = Path.GetFileName(directory);
|
||||||
|
int exitCode = await processInvoker.ExecuteAsync(_hc.GetDirectory(WellKnownDirectory.Bin), tar, $"-czf \"{archiveFile}\" -C \"{cwd}\" \"{inputDirectory}\"", null, CancellationToken.None);
|
||||||
|
if (exitCode != 0)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"Can't use 'tar -czf' to create archive file: {archiveFile}. return code: {exitCode}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return archiveFile;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLinkToActionArchive(string apiUrl, string repository, string @ref)
|
||||||
|
{
|
||||||
|
#if OS_WINDOWS
|
||||||
|
return $"{apiUrl}/repos/{repository}/zipball/{@ref}";
|
||||||
|
#else
|
||||||
|
return $"{apiUrl}/repos/{repository}/tarball/{@ref}";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private void Setup([CallerMemberName] string name = "")
|
private void Setup([CallerMemberName] string name = "")
|
||||||
{
|
{
|
||||||
_ecTokenSource?.Dispose();
|
_ecTokenSource?.Dispose();
|
||||||
@@ -1772,6 +1978,7 @@ runs:
|
|||||||
_hc.SetSingleton<IDockerCommandManager>(_dockerManager.Object);
|
_hc.SetSingleton<IDockerCommandManager>(_dockerManager.Object);
|
||||||
_hc.SetSingleton<IRunnerPluginManager>(_pluginManager.Object);
|
_hc.SetSingleton<IRunnerPluginManager>(_pluginManager.Object);
|
||||||
_hc.SetSingleton<IActionManifestManager>(actionManifest);
|
_hc.SetSingleton<IActionManifestManager>(actionManifest);
|
||||||
|
_hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());
|
||||||
|
|
||||||
_configurationStore = new Mock<IConfigurationStore>();
|
_configurationStore = new Mock<IConfigurationStore>();
|
||||||
_configurationStore
|
_configurationStore
|
||||||
|
|||||||
@@ -278,6 +278,59 @@ namespace GitHub.Runner.Common.Tests.Worker
|
|||||||
Assert.Equal("${{ matrix.node }}", _actionRunner.DisplayName);
|
Assert.Equal("${{ matrix.node }}", _actionRunner.DisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public async void WarnInvalidInputs()
|
||||||
|
{
|
||||||
|
//Arrange
|
||||||
|
Setup();
|
||||||
|
var actionId = Guid.NewGuid();
|
||||||
|
var actionInputs = new MappingToken(null, null, null);
|
||||||
|
actionInputs.Add(new StringToken(null, null, null, "input1"), new StringToken(null, null, null, "test1"));
|
||||||
|
actionInputs.Add(new StringToken(null, null, null, "input2"), new StringToken(null, null, null, "test2"));
|
||||||
|
actionInputs.Add(new StringToken(null, null, null, "invalid1"), new StringToken(null, null, null, "invalid1"));
|
||||||
|
actionInputs.Add(new StringToken(null, null, null, "invalid2"), new StringToken(null, null, null, "invalid2"));
|
||||||
|
var action = new Pipelines.ActionStep()
|
||||||
|
{
|
||||||
|
Name = "action",
|
||||||
|
Id = actionId,
|
||||||
|
Reference = new Pipelines.ContainerRegistryReference()
|
||||||
|
{
|
||||||
|
Image = "ubuntu:16.04"
|
||||||
|
},
|
||||||
|
Inputs = actionInputs
|
||||||
|
};
|
||||||
|
|
||||||
|
_actionRunner.Action = action;
|
||||||
|
|
||||||
|
Dictionary<string, string> finialInputs = new Dictionary<string, string>();
|
||||||
|
_handlerFactory.Setup(x => x.Create(It.IsAny<IExecutionContext>(), It.IsAny<ActionStepDefinitionReference>(), It.IsAny<IStepHost>(), It.IsAny<ActionExecutionData>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<Variables>(), It.IsAny<string>()))
|
||||||
|
.Callback((IExecutionContext executionContext, Pipelines.ActionStepDefinitionReference actionReference, IStepHost stepHost, ActionExecutionData data, Dictionary<string, string> inputs, Dictionary<string, string> environment, Variables runtimeVariables, string taskDirectory) =>
|
||||||
|
{
|
||||||
|
finialInputs = inputs;
|
||||||
|
})
|
||||||
|
.Returns(new Mock<IHandler>().Object);
|
||||||
|
|
||||||
|
//Act
|
||||||
|
await _actionRunner.RunAsync();
|
||||||
|
|
||||||
|
foreach (var input in finialInputs)
|
||||||
|
{
|
||||||
|
_hc.GetTrace().Info($"Input: {input.Key}={input.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
Assert.Equal("test1", finialInputs["input1"]);
|
||||||
|
Assert.Equal("test2", finialInputs["input2"]);
|
||||||
|
Assert.Equal("github", finialInputs["input3"]);
|
||||||
|
Assert.Equal("invalid1", finialInputs["invalid1"]);
|
||||||
|
Assert.Equal("invalid2", finialInputs["invalid2"]);
|
||||||
|
|
||||||
|
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Unexpected input 'invalid1'")), It.IsAny<string>()), Times.Once);
|
||||||
|
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Unexpected input 'invalid2'")), It.IsAny<string>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
private void Setup([CallerMemberName] string name = "")
|
private void Setup([CallerMemberName] string name = "")
|
||||||
{
|
{
|
||||||
_ecTokenSource?.Dispose();
|
_ecTokenSource?.Dispose();
|
||||||
|
|||||||
Reference in New Issue
Block a user