diff --git a/.github/workflows/create-azure-self-hosted-runners.yml b/.github/workflows/create-azure-self-hosted-runners.yml new file mode 100644 index 00000000..1ae6c548 --- /dev/null +++ b/.github/workflows/create-azure-self-hosted-runners.yml @@ -0,0 +1,145 @@ +name: create-azure-self-hosted-runners + +on: + workflow_dispatch: + inputs: + amount_of_runners: + description: 'Amount of runners to set up' + required: true + default: 1 + 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) + +env: + AMOUNT_OF_RUNNERS: ${{ github.event.inputs.amount_of_runners }} + 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 }}" + # This has to be a public URL that the VM can access after creation + POST_DEPLOYMENT_SCRIPT_URL: https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/azure-self-hosted-runners/post-deployment-script.ps1 + +# The following secrets are required for this workflow to run: +# AZURE_CREDENTIALS - Credentials for the Azure CLI. It's recommended to set up a resource +# group specifically for self-hosted Actions Runners. +# az ad sp create-for-rbac --name "{YOUR_DESCRIPTIVE_NAME_HERE}" --role contributor \ +# --scopes /subscriptions/{SUBSCRIPTION_ID_HERE}/resourceGroups/{RESOURCE_GROUP_HERE} \ +# --sdk-auth +# 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 +jobs: + create-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.create-matrix.outputs.matrix }} + steps: + - name: Create matrix for setting up runners in parallel + id: create-matrix + run: | + echo "Going to create $AMOUNT_OF_RUNNERS runners" + MATRIX="matrix={\"runner_index\":[$(seq -s "," 1 $AMOUNT_OF_RUNNERS)]}" + echo "Going to use this matrix: $MATRIX" + echo $MATRIX >> $GITHUB_OUTPUT + create-runners: + name: create-runner-${{ matrix.runner_index }} + needs: create-matrix + runs-on: ubuntu-latest + strategy: + matrix: ${{ fromJSON(needs.create-matrix.outputs.matrix) }} + 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@v3 + - name: Obtain installation token + id: setup + uses: actions/github-script@v6 + 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 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: | + case "$ACTIONS_RUNNER_SCOPE" in + "org-level") + ACTIONS_API_URL="https://api.github.com/repos/${{ env.ACTIONS_RUNNER_ORG }}/actions/runners/registration-token" + echo ACTIONS_RUNNER_REGISTRATION_URL="https://github.com/${{ env.ACTIONS_RUNNER_ORG }}" >> $GITHUB_ENV + ;; + "repo-level") + ACTIONS_API_URL="https://api.github.com/repos/${{ env.ACTIONS_RUNNER_ORG }}/${{ env.ACTIONS_RUNNER_REPO }}/actions/runners/registration-token" + echo ACTIONS_RUNNER_REGISTRATION_URL="https://github.com/${{ env.ACTIONS_RUNNER_ORG }}/${{ env.ACTIONS_RUNNER_REPO }}" >> $GITHUB_ENV + ;; + *) + 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" + echo ACTIONS_RUNNER_TOKEN=$ACTIONS_RUNNER_TOKEN >> $GITHUB_ENV + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - uses: azure/arm-deploy@v1 + with: + resourceGroupName: ${{ secrets.AZURE_RESOURCE_GROUP }} + template: ./azure-self-hosted-runners/azure-arm-template.json + parameters: ./azure-self-hosted-runners/azure-arm-template-example-parameters.json githubActionsRunnerRegistrationUrl="${{ env.ACTIONS_RUNNER_REGISTRATION_URL }}" githubActionsRunnerToken="${{ env.ACTIONS_RUNNER_TOKEN }}" postDeploymentPsScriptUrl="${{ env.POST_DEPLOYMENT_SCRIPT_URL }}" virtualMachineName=${{ steps.generate-vm-name.outputs.vm_name }} virtualMachineSize=Standard_D8pls_v5 publicIpAddressName1=${{ steps.generate-vm-name.outputs.vm_name }}-ip adminUsername=${{ secrets.AZURE_VM_USERNAME }} adminPassword=${{ secrets.AZURE_VM_PASSWORD }} + + - name: Deallocate the VM for later use + uses: azure/CLI@v1 + with: + azcliversion: 2.43.0 + inlineScript: | + az vm deallocate -n ${{ steps.generate-vm-name.outputs.vm_name }} -g ${{ secrets.AZURE_RESOURCE_GROUP }} --verbose diff --git a/azure-self-hosted-runners/azure-arm-template-example-parameters.json b/azure-self-hosted-runners/azure-arm-template-example-parameters.json new file mode 100644 index 00000000..627d90ab --- /dev/null +++ b/azure-self-hosted-runners/azure-arm-template-example-parameters.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "location": { + "value": "westeurope" + }, + "enableAcceleratedNetworking": { + "value": true + }, + "networkSecurityGroupRules": { + "value": [ + { + "name": "RDP", + "properties": { + "priority": 300, + "protocol": "TCP", + "access": "Allow", + "direction": "Inbound", + "sourceAddressPrefix": "*", + "sourcePortRange": "*", + "destinationAddressPrefix": "*", + "destinationPortRange": "3389" + } + } + ] + }, + "subnetName": { + "value": "default" + }, + "addressPrefixes": { + "value": [ + "10.2.0.0/16" + ] + }, + "subnets": { + "value": [ + { + "name": "default", + "properties": { + "addressPrefix": "10.2.0.0/24" + } + } + ] + }, + "publicIpAddressType": { + "value": "Static" + }, + "publicIpAddressSku": { + "value": "Standard" + }, + "pipDeleteOption": { + "value": "Detach" + }, + "osDiskType": { + "value": "Premium_LRS" + }, + "osDiskDeleteOption": { + "value": "Delete" + }, + "nicDeleteOption": { + "value": "Detach" + }, + "patchMode": { + "value": "AutomaticByOS" + }, + "enableHotpatching": { + "value": false + }, + "zone": { + "value": "1" + }, + "computerName": { + "value": "actions-runner" + } + } +} diff --git a/azure-self-hosted-runners/azure-arm-template.json b/azure-self-hosted-runners/azure-arm-template.json new file mode 100644 index 00000000..cc4882b5 --- /dev/null +++ b/azure-self-hosted-runners/azure-arm-template.json @@ -0,0 +1,268 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "githubActionsRunnerRegistrationUrl": { + "type": "string", + "minLength": 6, + "metadata": { + "description": "GitHub Actions Runner repo. E.g. https://github.com/MY_ORG (org-level) or https://github.com/MY_ORG/MY_REPO or (repo-level)" + } + }, + "githubActionsRunnerToken": { + "type": "securestring", + "minLength": 6, + "metadata": { + "description": "GitHub Actions Runner registration token for the org/repo. Note that these tokens are only valid for one hour after creation!" + } + }, + "postDeploymentPsScriptUrl": { + "type": "string", + "minLength": 6, + "metadata": { + "description": "URL to the post-deployment PowerShell script. E.g. https://raw.githubusercontent.com/git-for-windows/git-for-windows-automation/main/azure-self-hosted-runners/post-deployment-script.ps1" + } + }, + "computerName": { + "type": "string", + "maxLength": 15, + "metadata": { + "description": "Windows Computer Name. Can be maximum 15 characters." + } + }, + "location": { + "type": "string" + }, + "enableAcceleratedNetworking": { + "type": "bool" + }, + "networkSecurityGroupRules": { + "type": "array" + }, + "subnetName": { + "type": "string" + }, + "addressPrefixes": { + "type": "array" + }, + "subnets": { + "type": "array" + }, + "publicIpAddressName1": { + "type": "string" + }, + "publicIpAddressType": { + "type": "string" + }, + "publicIpAddressSku": { + "type": "string" + }, + "pipDeleteOption": { + "type": "string" + }, + "virtualMachineName": { + "type": "string" + }, + "osDiskType": { + "type": "string" + }, + "osDiskDeleteOption": { + "type": "string" + }, + "virtualMachineSize": { + "type": "string" + }, + "nicDeleteOption": { + "type": "string" + }, + "adminUsername": { + "type": "string" + }, + "adminPassword": { + "type": "securestring" + }, + "patchMode": { + "type": "string" + }, + "enableHotpatching": { + "type": "bool" + }, + "zone": { + "type": "string" + } + }, + "variables": { + "nsgName": "[concat(parameters('virtualMachineName'), '-nsg')]", + "nicName": "[concat(parameters('virtualMachineName'), '-nic')]", + "vnetName": "[concat(parameters('virtualMachineName'), '-vnet')]", + "vnetId": "[resourceId(resourceGroup().name,'Microsoft.Network/virtualNetworks', concat(parameters('virtualMachineName'), '-vnet'))]", + "subnetRef": "[concat(variables('vnetId'), '/subnets/', parameters('subnetName'))]" + }, + "resources": [ + { + "name": "[variables('nicName')]", + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2021-03-01", + "location": "[parameters('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkSecurityGroups/', variables('nsgName'))]", + "[concat('Microsoft.Network/virtualNetworks/', variables('vnetName'))]", + "[concat('Microsoft.Network/publicIpAddresses/', parameters('publicIpAddressName1'))]" + ], + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "subnet": { + "id": "[variables('subnetRef')]" + }, + "privateIPAllocationMethod": "Dynamic", + "publicIpAddress": { + "id": "[resourceId(resourceGroup().name, 'Microsoft.Network/publicIpAddresses', parameters('publicIpAddressName1'))]", + "properties": { + "deleteOption": "[parameters('pipDeleteOption')]" + } + } + } + } + ], + "enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]", + "networkSecurityGroup": { + "id": "[resourceId(resourceGroup().name, 'Microsoft.Network/networkSecurityGroups', variables('nsgName'))]" + } + } + }, + { + "name": "[variables('nsgName')]", + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2019-02-01", + "location": "[parameters('location')]", + "properties": { + "securityRules": "[parameters('networkSecurityGroupRules')]" + } + }, + { + "name": "[variables('vnetName')]", + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2020-11-01", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": "[parameters('addressPrefixes')]" + }, + "subnets": "[parameters('subnets')]" + } + }, + { + "name": "[parameters('publicIpAddressName1')]", + "type": "Microsoft.Network/publicIpAddresses", + "apiVersion": "2020-08-01", + "location": "[parameters('location')]", + "properties": { + "publicIpAllocationMethod": "[parameters('publicIpAddressType')]" + }, + "sku": { + "name": "[parameters('publicIpAddressSku')]" + }, + "zones": [ + "1" + ] + }, + { + "name": "[parameters('virtualMachineName')]", + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-07-01", + "location": "[parameters('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('virtualMachineSize')]" + }, + "storageProfile": { + "osDisk": { + "createOption": "fromImage", + "managedDisk": { + "storageAccountType": "[parameters('osDiskType')]" + }, + "deleteOption": "[parameters('osDiskDeleteOption')]" + }, + "imageReference": { + "publisher": "microsoftwindowsdesktop", + "offer": "windows11preview-arm64", + "sku": "win11-21h2-ent", + "version": "latest" + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]", + "properties": { + "deleteOption": "[parameters('nicDeleteOption')]" + } + } + ] + }, + "osProfile": { + "computerName": "[parameters('computerName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]", + "windowsConfiguration": { + "enableAutomaticUpdates": true, + "provisionVmAgent": true, + "patchSettings": { + "enableHotpatching": "[parameters('enableHotpatching')]", + "patchMode": "[parameters('patchMode')]" + } + } + }, + "licenseType": "Windows_Client", + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": true + } + } + }, + "zones": [ + "1" + ] + }, + { + "name": "Microsoft.CustomScriptExtension", + "apiVersion": "2015-01-01", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "incremental", + "templateLink": { + "uri": "https://catalogartifact.azureedge.net/publicartifacts/Microsoft.CustomScriptExtension-arm-2.0.57/MainTemplate.json" + }, + "parameters": { + "fileUris": { + "value": "[parameters('postDeploymentPsScriptUrl')]" + }, + "vmName": { + "value": "[parameters('virtualMachineName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "arguments": { + "value": "[concat('-GitHubActionsRunnerToken ', parameters('githubActionsRunnerToken'), ' -GithubActionsRunnerRegistrationUrl ', parameters('githubActionsRunnerRegistrationUrl'), ' -GithubActionsRunnerName ', parameters('virtualMachineName'))]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Compute/virtualMachines/', parameters('virtualMachineName'))]" + ] + } + ], + "outputs": { + "adminUsername": { + "type": "string", + "value": "[parameters('adminUsername')]" + } + } +} diff --git a/azure-self-hosted-runners/post-deployment-script.ps1 b/azure-self-hosted-runners/post-deployment-script.ps1 new file mode 100644 index 00000000..52ef7200 --- /dev/null +++ b/azure-self-hosted-runners/post-deployment-script.ps1 @@ -0,0 +1,121 @@ +param ( + # GitHub Actions Runner registration token. Note that these tokens are only valid for one hour after creation, so we always expect the user to provide one. + # https://docs.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners + [Parameter(Mandatory=$true)] + [string]$GitHubActionsRunnerToken, + + # GitHub Actions Runner repository. E.g. "https://github.com/MY_ORG" (org-level) or "https://github.com/MY_ORG/MY_REPO" (repo-level) + # https://docs.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners + [Parameter(Mandatory=$true)] + [string]$GithubActionsRunnerRegistrationUrl, + + # Actions Runner name. Needs to be unique in the org/repo + [Parameter(Mandatory=$true)] + [string]$GithubActionsRunnerName +) + +Write-Output "Starting post-deployment script." + +# ================================= +# TOOL VERSIONS AND OTHER VARIABLES +# ================================= + +$GitForWindowsVersion = "2.39.0" +$GitForWindowsTag = "2.39.0.windows.1" +$GitForWindowsHash = "2eaba567e17784654be77ba997329742d87845c6f15e33c9620f9a331c69a976" +# Note that the GitHub Actions Runner auto-updates itself by default, but do try to reference a relatively new version here. +$GitHubActionsRunnerVersion = "2.300.2" +$GithubActionsRunnerArch = "arm64" +$GithubActionsRunnerHash = "9409e50d9ad33d8031355ed079b8f56cf3699f35cf5d0ca51e54deed432758ef" +$GithubActionsRunnerLabels = "self-hosted,Windows,ARM64" +# Keep this path short to prevent Long Path issues +$GitHubActionsRunnerPath = "C:\actions-runner" + +# ====================== +# WINDOWS DEVELOPER MODE +# ====================== + +# Needed for symlink support +Write-Output "Enabling Windows Developer Mode..." +Start-Process -Wait "reg" 'add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"' +Write-Output "Enabled Windows developer mode." + +# ============================= +# MICROSOFT DEFENDER EXCLUSIONS +# ============================= + +Write-Output "Adding Microsoft Defender Exclusions..." +Add-MpPreference -ExclusionPath "C:\" +Write-Output "Finished adding Microsoft Defender Exclusions." + +# ====================== +# GIT FOR WINDOWS +# ====================== + +Write-Output "Downloading Git for Windows..." +$GitForWindowsOutputFile = "./git-for-windows-installer.exe" +$ProgressPreference = 'SilentlyContinue' +Invoke-WebRequest -UseBasicParsing -Uri "https://github.com/git-for-windows/git/releases/download/v${GitForWindowsTag}/Git-${GitForWindowsVersion}-64-bit.exe" -OutFile $GitForWindowsOutputFile +$ProgressPreference = 'Continue' + +if((Get-FileHash -Path $GitForWindowsOutputFile -Algorithm SHA256).Hash.ToUpper() -ne $GitForWindowsHash.ToUpper()){ throw 'Computed checksum did not match' } + +Write-Output "Installing Git for Windows..." +@" +[Setup] +Lang=default +Dir=C:\Program Files\Git +Group=Git +NoIcons=0 +SetupType=default +Components=gitlfs,windowsterminal +Tasks= +EditorOption=VIM +CustomEditorPath= +DefaultBranchOption= +PathOption=CmdTools +SSHOption=OpenSSH +TortoiseOption=false +CURLOption=WinSSL +CRLFOption=CRLFAlways +BashTerminalOption=ConHost +GitPullBehaviorOption=FFOnly +UseCredentialManager=Core +PerformanceTweaksFSCache=Enabled +EnableSymlinks=Disabled +EnablePseudoConsoleSupport=Disabled +EnableFSMonitor=Disabled +"@ | Out-File -FilePath "./git-installer-config.inf" + +Start-Process -Wait $GitForWindowsOutputFile '/VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /LOADINF="./git-installer-config.inf"' + +Write-Output "Finished installing Git for Windows." + +# ====================== +# GITHUB ACTIONS RUNNER +# ====================== + +Write-Output "Downloading GitHub Actions runner..." + +mkdir $GitHubActionsRunnerPath | Out-Null +$ProgressPreference = 'SilentlyContinue' +Invoke-WebRequest -UseBasicParsing -Uri https://github.com/actions/runner/releases/download/v${GitHubActionsRunnerVersion}/actions-runner-win-${GithubActionsRunnerArch}-${GitHubActionsRunnerVersion}.zip -OutFile ${GitHubActionsRunnerPath}\actions-runner-win-${GithubActionsRunnerArch}-${GitHubActionsRunnerVersion}.zip +$ProgressPreference = 'Continue' +if((Get-FileHash -Path ${GitHubActionsRunnerPath}\actions-runner-win-${GithubActionsRunnerArch}-${GitHubActionsRunnerVersion}.zip -Algorithm SHA256).Hash.ToUpper() -ne $GithubActionsRunnerHash.ToUpper()){ throw 'Computed checksum did not match' } + +Write-Output "Installing GitHub Actions runner ${GitHubActionsRunnerVersion} as a Windows service with labels ${GithubActionsRunnerLabels}..." + +Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory("${GitHubActionsRunnerPath}\actions-runner-win-${GithubActionsRunnerArch}-${GitHubActionsRunnerVersion}.zip", $GitHubActionsRunnerPath) +cmd.exe /c "${GitHubActionsRunnerPath}\config.cmd" --unattended --ephemeral --name ${GithubActionsRunnerName} --runasservice --labels ${GithubActionsRunnerLabels} --url ${GithubActionsRunnerRegistrationUrl} --token ${GitHubActionsRunnerToken} + +# Ensure that the service was created. If not, exit with error code. +$MatchedServices = Get-Service -Name "actions.runner.*" +if ($MatchedServices.count -eq 0) { + Write-Error "GitHub Actions service not found (should start with actions.runner). Check the logs in ${GitHubActionsRunnerPath}\_diag for more details." + exit 1 +} + +# Immediately stop the service as we want to leave the VM in a deallocated state for later use. The service will automatically be started when Windows starts. +Stop-Service -Name "actions.runner.*" -Verbose + +Write-Output "Finished installing GitHub Actions runner."