create-azure-self-hosted-runners #775
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: create-azure-self-hosted-runners | |
on: | |
workflow_dispatch: | |
inputs: | |
runner_scope: | |
type: choice | |
required: true | |
description: Scope of the runner. On personal accounts, only "repo-level" works | |
options: | |
- org-level | |
- repo-level | |
default: repo-level | |
runner_org: | |
type: string | |
required: false | |
description: Organization or personal account to deploy the runner to (defaults to the repository owner) | |
runner_repo: | |
type: string | |
required: false | |
description: Repo to deploy the runner to. Only needed if runner_scope is set to "repo-level" (defaults to current repository) | |
deallocate_immediately: | |
type: choice | |
options: | |
- false | |
- true | |
required: true | |
description: Deallocate the runner immediately after creating it (useful for spinning up runners preemptively) | |
default: "false" | |
ephemeral: | |
type: choice | |
options: | |
- false | |
- true | |
required: true | |
description: Start the runner in ephemeral mode (i.e. unregister after running one job) | |
default: "true" | |
env: | |
ACTIONS_RUNNER_SCOPE: ${{ github.event.inputs.runner_scope }} | |
ACTIONS_RUNNER_ORG: "${{ github.event.inputs.runner_org || github.repository_owner }}" | |
ACTIONS_RUNNER_REPO: "${{ github.event.inputs.runner_repo || github.event.repository.name }}" | |
DEALLOCATE_IMMEDIATELY: ${{ github.event.inputs.deallocate_immediately }} | |
EPHEMERAL_RUNNER: ${{ github.event.inputs.ephemeral }} | |
# Note that you'll need "p" (arm64 processor) and ideally "d" (local temp disk). The number 4 stands for 4 CPU-cores. | |
# For a convenient overview of all arm64 VM types, see e.g. https://azureprice.net/?_cpuArchitecture=Arm64 | |
AZURE_VM_TYPE: Standard_D4plds_v5 | |
# At the time of writing, "eastus", "eastus2" and "westus2" were among the cheapest region for the VM type we're using. | |
# For more information, see https://learn.microsoft.com/en-us/azure/virtual-machines/dplsv5-dpldsv5-series (which | |
# unfortunately does not have more information about price by region) | |
AZURE_VM_REGION: westus2 | |
AZURE_VM_IMAGE: win11-24h2-ent | |
permissions: | |
id-token: write # required for Azure login via OIDC | |
contents: read | |
# The following secrets are required for this workflow to run: | |
# AZURE_CLIENT_ID - The Client ID of an Azure Managed Identity. It is recommended to set up a resource | |
# group specifically for self-hosted Actions Runners, and to add a federated identity | |
# to authenticate as the currently-running GitHub workflow. | |
# az identity create --name <managed-identity-name> -g <resource-group> | |
# az identity federated-credential create \ | |
# --identity-name <managed-identity-name> \ | |
# --resource-group <resource-group> \ | |
# --name github-workflow \ | |
# --issuer https://token.actions.githubusercontent.com \ | |
# --subject repo:git-for-windows/git-for-windows-automation:ref:refs/heads/main \ | |
# --audiences api://AzureADTokenExchange | |
# MSYS_NO_PATHCONV=1 \ | |
# az role assignment create \ | |
# --assignee <client-id-of-managed-identity> \ | |
# --scope '/subscriptions/<subscription-id>/resourceGroups/<resource-group>' \ | |
# --role 'Contributor' | |
# AZURE_TENANT_ID - The Tenant ID of the Azure Managed Identity (i.e. the Azure Active Directory in which | |
# the Identity lives) | |
# AZURE_SUBSCRIPTION_ID - The Subscription ID with which the Azure Managed Identity is associated | |
# (technically, this is not necessary for `az login --service-principal` with a | |
# managed identity, but `Azure/login` requires it anyway) | |
# AZURE_RESOURCE_GROUP - Resource group to create the runner(s) in | |
# AZURE_VM_USERNAME - Username of the VM so you can RDP into it | |
# AZURE_VM_PASSWORD - Password of the VM so you can RDP into it | |
# GH_APP_ID - The ID of the GitHub App whose credentials are to be used to obtain the runner token | |
# GH_APP_PRIVATE_KEY - The private key of the GitHub App whose credentials are to be used to obtain the runner token | |
jobs: | |
create-runner: | |
runs-on: ubuntu-latest | |
outputs: | |
vm_name: ${{ steps.generate-vm-name.outputs.vm_name }} | |
steps: | |
- name: Generate VM name | |
id: generate-vm-name | |
run: | | |
VM_NAME="actions-runner-$(date +%Y%m%d%H%M%S%N)" | |
echo "Will be using $VM_NAME as the VM name" | |
echo "vm_name=$VM_NAME" >> $GITHUB_OUTPUT | |
- uses: actions/checkout@v4 | |
- name: Obtain installation token | |
id: setup | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const appId = ${{ secrets.GH_APP_ID }} | |
const privateKey = `${{ secrets.GH_APP_PRIVATE_KEY }}` | |
const getAppInstallationId = require('./get-app-installation-id') | |
const installationId = await getAppInstallationId( | |
console, | |
appId, | |
privateKey, | |
process.env.ACTIONS_RUNNER_ORG, | |
process.env.ACTIONS_RUNNER_REPO | |
) | |
const getInstallationAccessToken = require('./get-installation-access-token') | |
const { token: accessToken } = await getInstallationAccessToken( | |
console, | |
appId, | |
privateKey, | |
installationId | |
) | |
core.setSecret(accessToken) | |
core.setOutput('token', accessToken) | |
# We can't use the octokit/request-action as we can't properly mask the runner token with it | |
# https://github.com/actions/runner/issues/475 | |
- name: Generate Actions Runner token and registration URL | |
run: | | |
# We need to URL-encode the user name because it usually is a GitHub App, which means that | |
# it has the suffix `[bot]`. If un-encoded, this would cause a cURL error "bad range in URL" | |
# because it would mistake this for an IPv6 address or something like that. | |
user_pwd="$(jq -n \ | |
--arg user '${{ github.actor }}' \ | |
--arg pwd '${{ secrets.GITHUB_TOKEN }}' \ | |
'$user | @uri + ":" + $pwd')" | |
case "$ACTIONS_RUNNER_SCOPE" in | |
"org-level") | |
ACTIONS_API_URL="https://[email protected]/repos/$ACTIONS_RUNNER_ORG/actions/runners/registration-token" | |
ACTIONS_RUNNER_REGISTRATION_URL="https://[email protected]/$ACTIONS_RUNNER_ORG" | |
;; | |
"repo-level") | |
ACTIONS_API_URL="https://[email protected]/repos/$ACTIONS_RUNNER_ORG/$ACTIONS_RUNNER_REPO/actions/runners/registration-token" | |
ACTIONS_RUNNER_REGISTRATION_URL="https://[email protected]/$ACTIONS_RUNNER_ORG/$ACTIONS_RUNNER_REPO" | |
;; | |
*) | |
echo "Unsupported runner scope: $ACTIONS_RUNNER_SCOPE" | |
exit 1 | |
;; | |
esac | |
ACTIONS_RUNNER_TOKEN=$(curl \ | |
-X POST \ | |
-H "Accept: application/vnd.github+json" \ | |
-H "Authorization: Bearer ${{ steps.setup.outputs.token }}"\ | |
-H "X-GitHub-Api-Version: 2022-11-28" \ | |
$ACTIONS_API_URL \ | |
| jq --raw-output .token) | |
echo "::add-mask::$ACTIONS_RUNNER_TOKEN" | |
# The Azure VM type we use has blazing-fast local, temporary storage available as the D:\ drive. | |
# The only downside is that, after dellocation, the contents of this disk (including the Actions Runner), | |
# are destroyed. Let's only use it when we don't immediately deallocate the VM. | |
if [[ "$DEALLOCATE_IMMEDIATELY" == "true" ]]; then | |
ACTIONS_RUNNER_PATH="C:\a" | |
else | |
ACTIONS_RUNNER_PATH="D:\a" | |
fi | |
# Zip up and Base64-encode the post-deployment script; We used to provide a public URL | |
# for that script instead, but that does not work in private repositories (and we could | |
# not even use the `GITHUB_TOKEN` to access the file because it lacks the necessary | |
# scope to read repository contents). | |
POST_DEPLOYMENT_SCRIPT_ZIP_BASE64="$( | |
cd azure-self-hosted-runners && | |
zip -9 tmp.zip post-deployment-script.ps1 >&2 && | |
base64 -w 0 tmp.zip | |
)" | |
PUBLIC_IP_ADDRESS_NAME1="${{ github.repository_visibility != 'private' && format('{0}-ip', steps.generate-vm-name.outputs.vm_name) || '' }}" | |
AZURE_ARM_PARAMETERS=$(tr '\n' ' ' <<-END | |
githubActionsRunnerRegistrationUrl="$ACTIONS_RUNNER_REGISTRATION_URL" | |
githubActionsRunnerToken="$ACTIONS_RUNNER_TOKEN" | |
postDeploymentScriptZipBase64="$POST_DEPLOYMENT_SCRIPT_ZIP_BASE64" | |
postDeploymentScriptFileName="post-deployment-script.ps1" | |
virtualMachineImage="$AZURE_VM_IMAGE" | |
virtualMachineName="${{ steps.generate-vm-name.outputs.vm_name }}" | |
virtualMachineSize="$AZURE_VM_TYPE" | |
publicIpAddressName1="$PUBLIC_IP_ADDRESS_NAME1" | |
adminUsername="${{ secrets.AZURE_VM_USERNAME }}" | |
adminPassword="${{ secrets.AZURE_VM_PASSWORD }}" | |
ephemeral="$EPHEMERAL_RUNNER" | |
stopService="$DEALLOCATE_IMMEDIATELY" | |
githubActionsRunnerPath="$ACTIONS_RUNNER_PATH" | |
location="$AZURE_VM_REGION" | |
END | |
) | |
echo "AZURE_ARM_PARAMETERS=$AZURE_ARM_PARAMETERS" >> $GITHUB_ENV | |
- name: Azure Login | |
uses: azure/login@v2 | |
with: | |
client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
- uses: azure/arm-deploy@v2 | |
id: deploy-arm-template | |
with: | |
resourceGroupName: ${{ secrets.AZURE_RESOURCE_GROUP }} | |
deploymentName: deploy-${{ steps.generate-vm-name.outputs.vm_name }} | |
template: ./azure-self-hosted-runners/azure-arm-template.json | |
parameters: ./azure-self-hosted-runners/azure-arm-template-example-parameters.json ${{ env.AZURE_ARM_PARAMETERS }} | |
scope: resourcegroup | |
- name: Show some more information on failure | |
if: failure() | |
run: | | |
echo "::group::VM status" | |
az vm get-instance-view --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --name ${{ steps.generate-vm-name.outputs.vm_name }} --query "instanceView.statuses" | |
az vm get-instance-view --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --name ${{ steps.generate-vm-name.outputs.vm_name }} --query "statuses" | |
echo "::endgroup::" | |
echo "::group::Deployment logs" | |
az group deployment show --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --name deploy-${{ steps.generate-vm-name.outputs.vm_name }} | |
echo "::endgroup::" | |
echo "::group::Extension logs" | |
az vm extension show --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --vm-name ${{ steps.generate-vm-name.outputs.vm_name }} --name CustomScriptExtension | |
echo "::endgroup::" | |
- name: Show post-deployment script output | |
if: always() | |
env: | |
CUSTOM_SCRIPT_OUTPUT: ${{ steps.deploy-arm-template.outputs.customScriptInstanceView }} | |
run: echo "$CUSTOM_SCRIPT_OUTPUT" | jq -r '.substatuses[0].message' | sed 's/${{ secrets.GITHUB_TOKEN }}/***/g' | |
- name: Deallocate the VM for later use | |
if: env.DEALLOCATE_IMMEDIATELY == 'true' | |
run: az vm deallocate -n ${{ steps.generate-vm-name.outputs.vm_name }} -g ${{ secrets.AZURE_RESOURCE_GROUP }} --verbose |