Showing preview only (1,713K chars total). Download the full file or copy to clipboard to get everything.
Repository: actions/runner-images
Branch: main
Commit: 563ad669b646
Files: 504
Total size: 1.5 MB
Directory structure:
gitextract_ol0xcjy2/
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── announcement.yml
│ │ ├── bug-report.yml
│ │ ├── config.yml
│ │ └── tool-request.yml
│ ├── copilot-instructions.md
│ ├── pull_request_template.md
│ └── workflows/
│ ├── check-pinned-versions.yml
│ ├── codeql-analysis.yml
│ ├── create_github_release.yml
│ ├── create_pull_request.yml
│ ├── create_sbom_report.yml
│ ├── docker-images.yml
│ ├── linter.yml
│ ├── merge_pull_request.yml
│ ├── powershell-tests.yml
│ ├── trigger-ubuntu-win-build.yml
│ ├── ubuntu2204.yml
│ ├── ubuntu2404.yml
│ ├── update_github_release.yml
│ ├── validate-json-schema.yml
│ ├── windows2022.yml
│ ├── windows2025-vs2026.yml
│ └── windows2025.yml
├── .gitignore
├── .vscode/
│ ├── extensions.json
│ ├── settings.json
│ └── tasks.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── docs/
│ ├── create-image-and-azure-resources.md
│ └── dotnet-ubuntu.md
├── helpers/
│ ├── CheckJsonSchema.ps1
│ ├── CheckOutdatedVersionPinning.ps1
│ ├── CreateAzureVMFromPackerTemplate.ps1
│ ├── GenerateResourcesAndImage.ps1
│ ├── GitHubApi.psm1
│ ├── WaitWorkflowCompletion.ps1
│ └── software-report-base/
│ ├── Calculate-ImagesDifference.ps1
│ ├── SoftwareReport.BaseNodes.psm1
│ ├── SoftwareReport.DifferenceCalculator.psm1
│ ├── SoftwareReport.DifferenceRender.psm1
│ ├── SoftwareReport.Nodes.psm1
│ ├── SoftwareReport.psm1
│ └── tests/
│ ├── SoftwareReport.Difference.E2E.Tests.ps1
│ ├── SoftwareReport.DifferenceCalculator.Unit.Tests.ps1
│ ├── SoftwareReport.DifferenceRender.Unit.Tests.ps1
│ ├── SoftwareReport.E2E.Tests.ps1
│ ├── SoftwareReport.Nodes.Unit.Tests.ps1
│ └── TestHelpers.psm1
├── images/
│ ├── macos/
│ │ ├── assets/
│ │ │ ├── add-certificate.swift
│ │ │ ├── auto-software-update-arm64.exp
│ │ │ ├── bashprofile
│ │ │ ├── bashrc
│ │ │ └── bootstrap-provisioner/
│ │ │ ├── change_password
│ │ │ ├── installNewProvisioner.sh
│ │ │ ├── kcpassword.py
│ │ │ └── setAutoLogin.sh
│ │ ├── macos-14-Readme.md
│ │ ├── macos-14-arm64-Readme.md
│ │ ├── macos-15-Readme.md
│ │ ├── macos-15-arm64-Readme.md
│ │ ├── macos-26-Readme.md
│ │ ├── macos-26-arm64-Readme.md
│ │ ├── scripts/
│ │ │ ├── build/
│ │ │ │ ├── Configure-Toolset.ps1
│ │ │ │ ├── Configure-Xcode-Simulators.ps1
│ │ │ │ ├── Install-Toolset.ps1
│ │ │ │ ├── Install-Xcode.ps1
│ │ │ │ ├── Update-XcodeSimulators.ps1
│ │ │ │ ├── configure-auto-updates.sh
│ │ │ │ ├── configure-autologin.sh
│ │ │ │ ├── configure-hostname.sh
│ │ │ │ ├── configure-machine.sh
│ │ │ │ ├── configure-ntpconf.sh
│ │ │ │ ├── configure-preimagedata.sh
│ │ │ │ ├── configure-shell.sh
│ │ │ │ ├── configure-ssh.sh
│ │ │ │ ├── configure-system.sh
│ │ │ │ ├── configure-tccdb-macos.sh
│ │ │ │ ├── configure-windows.sh
│ │ │ │ ├── configure-xcode.sh
│ │ │ │ ├── install-actions-cache.sh
│ │ │ │ ├── install-android-sdk.sh
│ │ │ │ ├── install-audiodevice.sh
│ │ │ │ ├── install-aws-tools.sh
│ │ │ │ ├── install-azcopy.sh
│ │ │ │ ├── install-bicep.sh
│ │ │ │ ├── install-chrome.sh
│ │ │ │ ├── install-cocoapods.sh
│ │ │ │ ├── install-codeql-bundle.sh
│ │ │ │ ├── install-common-utils.sh
│ │ │ │ ├── install-dotnet.sh
│ │ │ │ ├── install-edge.sh
│ │ │ │ ├── install-firefox.sh
│ │ │ │ ├── install-gcc.sh
│ │ │ │ ├── install-git.sh
│ │ │ │ ├── install-homebrew.sh
│ │ │ │ ├── install-llvm.sh
│ │ │ │ ├── install-mono.sh
│ │ │ │ ├── install-nginx.sh
│ │ │ │ ├── install-node.sh
│ │ │ │ ├── install-openjdk.sh
│ │ │ │ ├── install-openssl.sh
│ │ │ │ ├── install-php.sh
│ │ │ │ ├── install-postgresql.sh
│ │ │ │ ├── install-powershell.sh
│ │ │ │ ├── install-python.sh
│ │ │ │ ├── install-rosetta.sh
│ │ │ │ ├── install-ruby.sh
│ │ │ │ ├── install-rubygems.sh
│ │ │ │ ├── install-rust.sh
│ │ │ │ ├── install-safari.sh
│ │ │ │ ├── install-swiftlint.sh
│ │ │ │ ├── install-unxip.sh
│ │ │ │ ├── install-vcpkg.sh
│ │ │ │ └── install-xcode-clt.sh
│ │ │ ├── docs-gen/
│ │ │ │ ├── Generate-SoftwareReport.ps1
│ │ │ │ ├── SoftwareReport.Android.psm1
│ │ │ │ ├── SoftwareReport.Browsers.psm1
│ │ │ │ ├── SoftwareReport.Common.psm1
│ │ │ │ ├── SoftwareReport.Helpers.psm1
│ │ │ │ ├── SoftwareReport.Java.psm1
│ │ │ │ ├── SoftwareReport.Toolcache.psm1
│ │ │ │ └── SoftwareReport.Xcode.psm1
│ │ │ ├── helpers/
│ │ │ │ ├── Common.Helpers.psm1
│ │ │ │ ├── Xcode.Helpers.psm1
│ │ │ │ ├── Xcode.Installer.psm1
│ │ │ │ ├── confirm-identified-developers-macos14.scpt
│ │ │ │ ├── confirm-identified-developers-macos15.scpt
│ │ │ │ ├── invoke-tests.sh
│ │ │ │ └── utils.sh
│ │ │ └── tests/
│ │ │ ├── ActionArchiveCache.Tests.ps1
│ │ │ ├── Android.Tests.ps1
│ │ │ ├── BasicTools.Tests.ps1
│ │ │ ├── Browsers.Tests.ps1
│ │ │ ├── Common.Tests.ps1
│ │ │ ├── Git.Tests.ps1
│ │ │ ├── Helpers.psm1
│ │ │ ├── Java.Tests.ps1
│ │ │ ├── LLVM.Tests.ps1
│ │ │ ├── Linters.Tests.ps1
│ │ │ ├── Mono.Tests.ps1
│ │ │ ├── Node.Tests.ps1
│ │ │ ├── OpenSSL.Tests.ps1
│ │ │ ├── PHP.Tests.ps1
│ │ │ ├── Powershell.Tests.ps1
│ │ │ ├── Python.Tests.ps1
│ │ │ ├── Rosetta.Tests.ps1
│ │ │ ├── Ruby.Tests.ps1
│ │ │ ├── RubyGem.Tests.ps1
│ │ │ ├── RunAll-Tests.ps1
│ │ │ ├── Rust.Tests.ps1
│ │ │ ├── System.Tests.ps1
│ │ │ ├── Toolcache.Tests.ps1
│ │ │ ├── Toolset.Tests.ps1
│ │ │ └── Xcode.Tests.ps1
│ │ ├── templates/
│ │ │ ├── macOS-14.anka.pkr.hcl
│ │ │ ├── macOS-14.arm64.anka.pkr.hcl
│ │ │ ├── macOS-15.anka.pkr.hcl
│ │ │ ├── macOS-15.arm64.anka.pkr.hcl
│ │ │ ├── macOS-26.anka.pkr.hcl
│ │ │ └── macOS-26.arm64.anka.pkr.hcl
│ │ └── toolsets/
│ │ ├── Readme.md
│ │ ├── toolset-14.json
│ │ ├── toolset-15.json
│ │ └── toolset-26.json
│ ├── ubuntu/
│ │ ├── Ubuntu2204-Readme.md
│ │ ├── Ubuntu2404-Readme.md
│ │ ├── assets/
│ │ │ ├── post-gen/
│ │ │ │ ├── cleanup-logs.sh
│ │ │ │ ├── environment-variables.sh
│ │ │ │ └── systemd-linger.sh
│ │ │ └── ubuntu2204.conf
│ │ ├── scripts/
│ │ │ ├── build/
│ │ │ │ ├── Configure-Toolset.ps1
│ │ │ │ ├── Install-PowerShellAzModules.ps1
│ │ │ │ ├── Install-PowerShellModules.ps1
│ │ │ │ ├── Install-Toolset.ps1
│ │ │ │ ├── cleanup.sh
│ │ │ │ ├── configure-apt-mock.sh
│ │ │ │ ├── configure-apt-sources.sh
│ │ │ │ ├── configure-apt.sh
│ │ │ │ ├── configure-dpkg.sh
│ │ │ │ ├── configure-environment.sh
│ │ │ │ ├── configure-image-data.sh
│ │ │ │ ├── configure-limits.sh
│ │ │ │ ├── configure-snap.sh
│ │ │ │ ├── configure-system.sh
│ │ │ │ ├── install-actions-cache.sh
│ │ │ │ ├── install-aliyun-cli.sh
│ │ │ │ ├── install-android-sdk.sh
│ │ │ │ ├── install-apache.sh
│ │ │ │ ├── install-apt-common.sh
│ │ │ │ ├── install-apt-vital.sh
│ │ │ │ ├── install-aws-tools.sh
│ │ │ │ ├── install-azcopy.sh
│ │ │ │ ├── install-azure-cli.sh
│ │ │ │ ├── install-azure-devops-cli.sh
│ │ │ │ ├── install-bazel.sh
│ │ │ │ ├── install-bicep.sh
│ │ │ │ ├── install-clang.sh
│ │ │ │ ├── install-cmake.sh
│ │ │ │ ├── install-codeql-bundle.sh
│ │ │ │ ├── install-container-tools.sh
│ │ │ │ ├── install-docker.sh
│ │ │ │ ├── install-dotnetcore-sdk.sh
│ │ │ │ ├── install-firefox.sh
│ │ │ │ ├── install-gcc-compilers.sh
│ │ │ │ ├── install-gfortran.sh
│ │ │ │ ├── install-git-lfs.sh
│ │ │ │ ├── install-git.sh
│ │ │ │ ├── install-github-cli.sh
│ │ │ │ ├── install-google-chrome.sh
│ │ │ │ ├── install-google-cloud-cli.sh
│ │ │ │ ├── install-haskell.sh
│ │ │ │ ├── install-heroku.sh
│ │ │ │ ├── install-homebrew.sh
│ │ │ │ ├── install-java-tools.sh
│ │ │ │ ├── install-julia.sh
│ │ │ │ ├── install-kotlin.sh
│ │ │ │ ├── install-kubernetes-tools.sh
│ │ │ │ ├── install-leiningen.sh
│ │ │ │ ├── install-microsoft-edge.sh
│ │ │ │ ├── install-miniconda.sh
│ │ │ │ ├── install-mono.sh
│ │ │ │ ├── install-ms-repos.sh
│ │ │ │ ├── install-mssql-tools.sh
│ │ │ │ ├── install-mysql.sh
│ │ │ │ ├── install-nginx.sh
│ │ │ │ ├── install-ninja.sh
│ │ │ │ ├── install-nodejs.sh
│ │ │ │ ├── install-nvm.sh
│ │ │ │ ├── install-oc-cli.sh
│ │ │ │ ├── install-oras-cli.sh
│ │ │ │ ├── install-packer.sh
│ │ │ │ ├── install-php.sh
│ │ │ │ ├── install-pipx-packages.sh
│ │ │ │ ├── install-postgresql.sh
│ │ │ │ ├── install-powershell.sh
│ │ │ │ ├── install-pulumi.sh
│ │ │ │ ├── install-pypy.sh
│ │ │ │ ├── install-python.sh
│ │ │ │ ├── install-rlang.sh
│ │ │ │ ├── install-ruby.sh
│ │ │ │ ├── install-rust.sh
│ │ │ │ ├── install-sbt.sh
│ │ │ │ ├── install-selenium.sh
│ │ │ │ ├── install-sqlpackage.sh
│ │ │ │ ├── install-swift.sh
│ │ │ │ ├── install-terraform.sh
│ │ │ │ ├── install-vcpkg.sh
│ │ │ │ ├── install-yq.sh
│ │ │ │ ├── install-zstd.sh
│ │ │ │ ├── list-dpkg.sh
│ │ │ │ └── post-build-validation.sh
│ │ │ ├── docs-gen/
│ │ │ │ ├── Generate-SoftwareReport.ps1
│ │ │ │ ├── SoftwareReport.Android.psm1
│ │ │ │ ├── SoftwareReport.Browsers.psm1
│ │ │ │ ├── SoftwareReport.CachedTools.psm1
│ │ │ │ ├── SoftwareReport.Common.psm1
│ │ │ │ ├── SoftwareReport.Databases.psm1
│ │ │ │ ├── SoftwareReport.Helpers.psm1
│ │ │ │ ├── SoftwareReport.Java.psm1
│ │ │ │ ├── SoftwareReport.Rust.psm1
│ │ │ │ ├── SoftwareReport.Tools.psm1
│ │ │ │ └── SoftwareReport.WebServers.psm1
│ │ │ ├── helpers/
│ │ │ │ ├── Common.Helpers.psm1
│ │ │ │ ├── etc-environment.sh
│ │ │ │ ├── install.sh
│ │ │ │ ├── invoke-tests.sh
│ │ │ │ └── os.sh
│ │ │ └── tests/
│ │ │ ├── ActionArchiveCache.Tests.ps1
│ │ │ ├── Android.Tests.ps1
│ │ │ ├── Apt.Tests.ps1
│ │ │ ├── Browsers.Tests.ps1
│ │ │ ├── CLI.Tools.Tests.ps1
│ │ │ ├── Common.Tests.ps1
│ │ │ ├── Databases.Tests.ps1
│ │ │ ├── DotnetSDK.Tests.ps1
│ │ │ ├── Haskell.Tests.ps1
│ │ │ ├── Helpers.psm1
│ │ │ ├── Java.Tests.ps1
│ │ │ ├── Node.Tests.ps1
│ │ │ ├── PowerShellModules.Tests.ps1
│ │ │ ├── RunAll-Tests.ps1
│ │ │ ├── System.Tests.ps1
│ │ │ ├── Tools.Tests.ps1
│ │ │ ├── Toolset.Tests.ps1
│ │ │ └── WebServers.Tests.ps1
│ │ ├── templates/
│ │ │ ├── build.ubuntu-22_04.pkr.hcl
│ │ │ ├── build.ubuntu-24_04.pkr.hcl
│ │ │ ├── locals.ubuntu.pkr.hcl
│ │ │ ├── source.ubuntu.pkr.hcl
│ │ │ └── variable.ubuntu.pkr.hcl
│ │ └── toolsets/
│ │ ├── toolset-2204.json
│ │ └── toolset-2404.json
│ ├── ubuntu-slim/
│ │ ├── Dockerfile
│ │ ├── generate-software-report.sh
│ │ ├── scripts/
│ │ │ ├── build/
│ │ │ │ ├── configure-apt-sources.sh
│ │ │ │ ├── configure-apt.sh
│ │ │ │ ├── configure-dpkg.sh
│ │ │ │ ├── configure-environment.sh
│ │ │ │ ├── configure-image-data-file.sh
│ │ │ │ ├── configure-system.sh
│ │ │ │ ├── install-actions-cache.sh
│ │ │ │ ├── install-apt-common.sh
│ │ │ │ ├── install-apt-vital.sh
│ │ │ │ ├── install-aws-tools.sh
│ │ │ │ ├── install-azcopy.sh
│ │ │ │ ├── install-azure-cli.sh
│ │ │ │ ├── install-azure-devops-cli.sh
│ │ │ │ ├── install-bicep.sh
│ │ │ │ ├── install-docker-cli.sh
│ │ │ │ ├── install-git-lfs.sh
│ │ │ │ ├── install-git.sh
│ │ │ │ ├── install-github-cli.sh
│ │ │ │ ├── install-google-cloud-cli.sh
│ │ │ │ ├── install-ms-repos.sh
│ │ │ │ ├── install-nodejs.sh
│ │ │ │ ├── install-nvm.sh
│ │ │ │ ├── install-pipx-packages.sh
│ │ │ │ ├── install-powershell.sh
│ │ │ │ ├── install-python.sh
│ │ │ │ ├── install-yq.sh
│ │ │ │ └── install-zstd.sh
│ │ │ ├── docs-gen/
│ │ │ │ ├── Common.Helpers.psm1
│ │ │ │ ├── Generate-SoftwareReport.ps1
│ │ │ │ ├── SoftwareReport.Common.psm1
│ │ │ │ ├── SoftwareReport.Helpers.psm1
│ │ │ │ └── SoftwareReport.Tools.psm1
│ │ │ ├── entrypoint.sh
│ │ │ └── helpers/
│ │ │ ├── cleanup.sh
│ │ │ ├── etc-environment.sh
│ │ │ ├── install.sh
│ │ │ └── os.sh
│ │ ├── test.sh
│ │ ├── toolsets/
│ │ │ └── toolset.json
│ │ ├── ubuntu-slim-Readme.md
│ │ └── ubuntu-slim-Report.json
│ └── windows/
│ ├── Windows2022-Readme.md
│ ├── Windows2025-Readme.md
│ ├── Windows2025-VS2026-Readme.md
│ ├── assets/
│ │ └── post-gen/
│ │ ├── GenerateIISExpressCertificate.ps1
│ │ ├── InternetExplorerConfiguration.ps1
│ │ ├── Msys2FirstLaunch.ps1
│ │ ├── VSConfiguration.ps1
│ │ └── warmup.vdproj
│ ├── scripts/
│ │ ├── build/
│ │ │ ├── Configure-BaseImage.ps1
│ │ │ ├── Configure-DeveloperMode.ps1
│ │ │ ├── Configure-Diagnostics.ps1
│ │ │ ├── Configure-DotnetSecureChannel.ps1
│ │ │ ├── Configure-DynamicPort.ps1
│ │ │ ├── Configure-GDIProcessHandleQuota.ps1
│ │ │ ├── Configure-ImageDataFile.ps1
│ │ │ ├── Configure-PowerShell.ps1
│ │ │ ├── Configure-Shell.ps1
│ │ │ ├── Configure-System.ps1
│ │ │ ├── Configure-SystemEnvironment.ps1
│ │ │ ├── Configure-Toolset.ps1
│ │ │ ├── Configure-User.ps1
│ │ │ ├── Configure-WindowsDefender.ps1
│ │ │ ├── Install-AWSTools.ps1
│ │ │ ├── Install-ActionsCache.ps1
│ │ │ ├── Install-AliyunCli.ps1
│ │ │ ├── Install-AndroidSDK.ps1
│ │ │ ├── Install-Apache.ps1
│ │ │ ├── Install-AzureCli.ps1
│ │ │ ├── Install-AzureCosmosDbEmulator.ps1
│ │ │ ├── Install-AzureDevOpsCli.ps1
│ │ │ ├── Install-Bazel.ps1
│ │ │ ├── Install-Chocolatey.ps1
│ │ │ ├── Install-ChocolateyPackages.ps1
│ │ │ ├── Install-Chrome.ps1
│ │ │ ├── Install-CodeQLBundle.ps1
│ │ │ ├── Install-DACFx.ps1
│ │ │ ├── Install-Docker.ps1
│ │ │ ├── Install-DockerCompose.ps1
│ │ │ ├── Install-DockerWinCred.ps1
│ │ │ ├── Install-DotnetSDK.ps1
│ │ │ ├── Install-EdgeDriver.ps1
│ │ │ ├── Install-Firefox.ps1
│ │ │ ├── Install-Git.ps1
│ │ │ ├── Install-GitHub-CLI.ps1
│ │ │ ├── Install-Haskell.ps1
│ │ │ ├── Install-IEWebDriver.ps1
│ │ │ ├── Install-JavaTools.ps1
│ │ │ ├── Install-Kotlin.ps1
│ │ │ ├── Install-KubernetesTools.ps1
│ │ │ ├── Install-LLVM.ps1
│ │ │ ├── Install-Mercurial.ps1
│ │ │ ├── Install-Mingw64.ps1
│ │ │ ├── Install-Miniconda.ps1
│ │ │ ├── Install-MongoDB.ps1
│ │ │ ├── Install-Msys2.ps1
│ │ │ ├── Install-MysqlCli.ps1
│ │ │ ├── Install-NSIS.ps1
│ │ │ ├── Install-NativeImages.ps1
│ │ │ ├── Install-Nginx.ps1
│ │ │ ├── Install-NodeJS.ps1
│ │ │ ├── Install-OpenSSL.ps1
│ │ │ ├── Install-PHP.ps1
│ │ │ ├── Install-Pipx.ps1
│ │ │ ├── Install-PostgreSQL.ps1
│ │ │ ├── Install-PowerShellModules.ps1
│ │ │ ├── Install-PowershellAzModules.ps1
│ │ │ ├── Install-PowershellCore.ps1
│ │ │ ├── Install-PyPy.ps1
│ │ │ ├── Install-R.ps1
│ │ │ ├── Install-RootCA.ps1
│ │ │ ├── Install-Ruby.ps1
│ │ │ ├── Install-Rust.ps1
│ │ │ ├── Install-SQLOLEDBDriver.ps1
│ │ │ ├── Install-SQLPowerShellTools.ps1
│ │ │ ├── Install-Sbt.ps1
│ │ │ ├── Install-Selenium.ps1
│ │ │ ├── Install-ServiceFabricSDK.ps1
│ │ │ ├── Install-Stack.ps1
│ │ │ ├── Install-Toolset.ps1
│ │ │ ├── Install-TortoiseSvn.ps1
│ │ │ ├── Install-VSExtensions.ps1
│ │ │ ├── Install-Vcpkg.ps1
│ │ │ ├── Install-VisualStudio.ps1
│ │ │ ├── Install-WDK.ps1
│ │ │ ├── Install-WSL2.ps1
│ │ │ ├── Install-WebPlatformInstaller.ps1
│ │ │ ├── Install-WinAppDriver.ps1
│ │ │ ├── Install-WindowsFeatures.ps1
│ │ │ ├── Install-WindowsUpdates.ps1
│ │ │ ├── Install-WindowsUpdatesAfterReboot.ps1
│ │ │ ├── Install-Wix.ps1
│ │ │ ├── Install-Zstd.ps1
│ │ │ ├── Invoke-Cleanup.ps1
│ │ │ └── Post-Build-Validation.ps1
│ │ ├── docs-gen/
│ │ │ ├── Generate-SoftwareReport.ps1
│ │ │ ├── SoftwareReport.Android.psm1
│ │ │ ├── SoftwareReport.Browsers.psm1
│ │ │ ├── SoftwareReport.CachedTools.psm1
│ │ │ ├── SoftwareReport.Common.psm1
│ │ │ ├── SoftwareReport.Databases.psm1
│ │ │ ├── SoftwareReport.Helpers.psm1
│ │ │ ├── SoftwareReport.Java.psm1
│ │ │ ├── SoftwareReport.Tools.psm1
│ │ │ ├── SoftwareReport.VisualStudio.psm1
│ │ │ └── SoftwareReport.WebServers.psm1
│ │ ├── helpers/
│ │ │ ├── AndroidHelpers.ps1
│ │ │ ├── ChocoHelpers.ps1
│ │ │ ├── ImageHelpers.psd1
│ │ │ ├── ImageHelpers.psm1
│ │ │ ├── InstallHelpers.ps1
│ │ │ ├── PathHelpers.ps1
│ │ │ ├── VisualStudioHelpers.ps1
│ │ │ └── test/
│ │ │ └── ImageHelpers.Tests.ps1
│ │ └── tests/
│ │ ├── ActionArchiveCache.Tests.ps1
│ │ ├── Android.Tests.ps1
│ │ ├── Apache.Tests.ps1
│ │ ├── Browsers.Tests.ps1
│ │ ├── CLI.Tools.Tests.ps1
│ │ ├── ChocoPackages.Tests.ps1
│ │ ├── Databases.Tests.ps1
│ │ ├── Docker.Tests.ps1
│ │ ├── DotnetSDK.Tests.ps1
│ │ ├── Git.Tests.ps1
│ │ ├── Haskell.Tests.ps1
│ │ ├── Helpers.psm1
│ │ ├── Java.Tests.ps1
│ │ ├── LLVM.Tests.ps1
│ │ ├── MSYS2.Tests.ps1
│ │ ├── Miniconda.Tests.ps1
│ │ ├── Nginx.Tests.ps1
│ │ ├── Node.Tests.ps1
│ │ ├── PHP.Tests.ps1
│ │ ├── PipxPackages.Tests.ps1
│ │ ├── PowerShellAzModules.Tests.ps1
│ │ ├── PowerShellModules.Tests.ps1
│ │ ├── RunAll-Tests.ps1
│ │ ├── Rust.Tests.ps1
│ │ ├── SSDTExtensions.Tests.ps1
│ │ ├── Shell.Tests.ps1
│ │ ├── Tools.Tests.ps1
│ │ ├── Toolset.Tests.ps1
│ │ ├── VisualStudio.Tests.ps1
│ │ ├── Vsix.Tests.ps1
│ │ ├── WDK.Tests.ps1
│ │ ├── WinAppDriver.Tests.ps1
│ │ ├── WindowsFeatures.Tests.ps1
│ │ └── Wix.Tests.ps1
│ ├── templates/
│ │ ├── build.windows-2022.pkr.hcl
│ │ ├── build.windows-2025-vs2026.pkr.hcl
│ │ ├── build.windows-2025.pkr.hcl
│ │ ├── locals.windows.pkr.hcl
│ │ ├── source.windows.pkr.hcl
│ │ └── variable.windows.pkr.hcl
│ └── toolsets/
│ ├── toolset-2022.json
│ ├── toolset-2025-vs2026.json
│ └── toolset-2025.json
├── images.CI/
│ ├── credscan-exclusions.json
│ ├── linux-and-win/
│ │ ├── build-image.ps1
│ │ ├── cleanup.ps1
│ │ └── create-release.ps1
│ ├── measure-provisioners-duration.ps1
│ └── shebang-linter.ps1
└── schemas/
└── toolset-schema.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/CODEOWNERS
================================================
* @actions/runner-images-team
================================================
FILE: .github/ISSUE_TEMPLATE/announcement.yml
================================================
name: Announcement
description: Submit an announcement
labels: [Announcement]
body:
- type: textarea
attributes:
label: Breaking changes
placeholder: Short description of the upcoming change
validations:
required: true
- type: textarea
attributes:
label: Target date
placeholder: Date of changes propagation start
validations:
required: true
- type: textarea
attributes:
label: The motivation for the changes
placeholder: Description of main reasons for this change
validations:
required: true
- type: textarea
attributes:
label: Possible impact
placeholder: Description of who might be impacted by this change
validations:
required: true
- type: checkboxes
attributes:
label: Platforms affected
options:
- label: Azure DevOps
- label: GitHub Actions
- type: checkboxes
attributes:
label: Runner images affected
options:
- label: Ubuntu 22.04
- label: Ubuntu 24.04
- label: Ubuntu Slim
- label: macOS 14
- label: macOS 14 Arm64
- label: macOS 15
- label: macOS 15 Arm64
- label: macOS 26
- label: macOS 26 Arm64
- label: Windows Server 2022
- label: Windows Server 2025
- label: Windows Server 2025 with Visual Studio 2026
- type: textarea
attributes:
label: Mitigation ways
description: Steps or options for impact mitigation
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: Bug Report
description: Submit a bug report.
labels: [bug report, needs triage]
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of what the bug is, and why you consider it to be a bug.
validations:
required: true
- type: checkboxes
attributes:
label: Platforms affected
options:
- label: Azure DevOps
- label: GitHub Actions - Standard Runners
- label: GitHub Actions - Larger Runners
- type: checkboxes
attributes:
label: Runner images affected
options:
- label: Ubuntu 22.04
- label: Ubuntu 24.04
- label: Ubuntu Slim
- label: macOS 14
- label: macOS 14 Arm64
- label: macOS 15
- label: macOS 15 Arm64
- label: macOS 26
- label: macOS 26 Arm64
- label: Windows Server 2022
- label: Windows Server 2025
- label: Windows Server 2025 with Visual Studio 2026
- type: textarea
attributes:
label: Image version and build link
description: |
Image version where you are experiencing the issue. Where to find image version in build logs:
1. For GitHub Actions, under "Set up job" -> "Runner Image" -> "Version".
2. For Azure DevOps, under "Initialize job" -> "Runner Image" -> "Version".
If you have a public example, please, provide a link to the failed build.
validations:
required: true
- type: input
attributes:
label: Is it regression?
description: If yes, please, provide the latest image version where the issue didn't persist, and a link to the latest successful build.
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: A description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Actual behavior
description: A description of what is actually happening.
validations:
required: true
- type: textarea
attributes:
label: Repro steps
placeholder: |
A description with steps to reproduce the issue.
1. Step 1
2. Step 2
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Get help in GitHub Discussions
url: https://github.com/actions/runner-images/discussions
about: Have a question? Feel free to ask in the runner-images GitHub Discussions!
================================================
FILE: .github/ISSUE_TEMPLATE/tool-request.yml
================================================
name: Tool request
description: Request a new tool or update to a tool
title: Update/Add [tool name]
labels: [feature request, needs triage]
body:
- type: markdown
attributes:
value: "## Tool information"
- type: input
attributes:
label: Tool name
validations:
required: true
- type: input
attributes:
label: Tool license
description: Type of licensing for desired tool.
validations:
required: true
- type: checkboxes
attributes:
label: Add or update?
options:
- label: Add
- label: Update
- type: input
attributes:
label: Desired version
description: Let us know if you're requesting a specific version, dev/RC, whatever is latest, etc.
validations:
required: true
- type: input
attributes:
label: Approximate size
description: Leave blank if unknown.
- type: markdown
attributes:
value: "## If this is an add request"
- type: textarea
attributes:
label: Brief description of tool
- type: input
attributes:
label: URL for tool's homepage
- type: textarea
attributes:
label: Provide a basic test case to validate the tool's functionality.
description: This will be automatically formatted into code.
render: bash
- type: checkboxes
attributes:
label: Platforms where you need the tool
options:
- label: Azure DevOps
- label: GitHub Actions
- type: checkboxes
attributes:
label: Runner images where you need the tool
options:
- label: Ubuntu 22.04
- label: Ubuntu 24.04
- label: Ubuntu Slim
- label: macOS 14
- label: macOS 14 Arm64
- label: macOS 15
- label: macOS 15 Arm64
- label: macOS 26
- label: macOS 26 Arm64
- label: Windows Server 2022
- label: Windows Server 2025
- label: Windows Server 2025 with Visual Studio 2026
- type: textarea
attributes:
label: Can this tool be installed during the build?
description: If so, please provide a description with required steps. This will be automatically formatted into code.
render: bash
- type: input
attributes:
label: Tool installation time in runtime
description: How long does it take to install the tool?
- type: input
attributes:
label: Are you willing to submit a PR?
description: We accept contributions!
================================================
FILE: .github/copilot-instructions.md
================================================
# GitHub Copilot Instructions for Actions Runner Images Repository
## Scope and goals
- This repository serves as the source for building GitHub Actions runner and Azure DevOps agent images for Windows, Ubuntu, and macOS. You can find exact versions in the [Available Images](../README.md#available-images) section of README.md. Windows and Ubuntu images build on Azure infrastructure using Packer; macOS images use Anka virtualization.
- Emphasize best practices for contributing to open-source projects, including code style, commit messages, and pull request etiquette.
- Prefer clarity and correctness over creativity. If information is missing, ask clarifying questions or insert TODOs instead of guessing.
## Code and command instructions
- Follow the code style guide in [CONTRIBUTING.md](../CONTRIBUTING.md#code-style-guide) for Bash and PowerShell scripts, including naming conventions, file structure, and indentation rules.
- Focus on re-using helpers when writing scripts. Windows, Linux and Ubuntu scripts have helper functions available to simplify installation and validation.
- Always confirm versions and installation paths against existing toolset files and installation scripts.
## Output format
- Use GitHub Flavored Markdown only. Avoid raw HTML unless necessary.
- One H1 (`#`) per page, followed by logical, sequential headings (`##`, `###`, …).
- Use fenced code blocks with language identifiers (` ```bash `, ` ```json `, ` ```yaml `, etc.).
- Use blockquote callouts for notes:
> [!NOTE] Context or nuance
> [!TIP] Helpful hint
> [!WARNING] Risks or breaking changes
> [!IMPORTANT] Critical requirement for functionality
## Style and tone
- Audience: Open-source contributors, GitHub Actions maintainers, and developers building custom runner images. Assume familiarity with CI/CD concepts, Packer, and basic infrastructure provisioning, but explain platform-specific details (Azure for Windows/Ubuntu, Anka for macOS) when relevant.
- Voice: Second person ("you"), active voice, imperative for operational steps.
- Be concise: short paragraphs and sentences. Prefer lists and step-by-steps, especially for operational procedures and troubleshooting.
- Use inclusive, accessible language. Avoid idioms, sarcasm, and culturally specific references.
- English: en-US (spelling, punctuation, and units).
## Safety and integrity
- Do not expose sensitive credentials (API tokens, Azure subscription IDs, etc.) in code examples.
- Do not fabricate tool versions, installation paths, or software availability without verifying against toolset files or actual installation scripts.
- Always call out assumptions and limitations explicitly, especially for changes affecting runner image behavior or software availability.
- If ambiguous requests are made about image modifications, ask clarifying questions about target OS, tool versions, and compatibility requirements before proceeding.
================================================
FILE: .github/pull_request_template.md
================================================
# Description
New tool, Bug fixing, or Improvement?
Please include a summary of the change and which issue is fixed. Also include relevant motivation and context.
**For new tools, please provide total size and installation time.**
<!-- Currently, we can't accept external contributions to macOS source. Please find more details in [CONTRIBUTING.md](CONTRIBUTING.md#macOS) guide -->
#### Related issue:
## Check list
- [ ] Related issue / work item is attached
- [ ] Tests are written (if applicable)
- [ ] Documentation is updated (if applicable)
- [ ] Changes are tested and related VM images are successfully generated
================================================
FILE: .github/workflows/check-pinned-versions.yml
================================================
name: Check Outdated Version Pinning
on:
schedule:
- cron: '0 12 * * 1' # Run at 12:00 UTC every Monday
permissions:
issues: write
contents: read
jobs:
check-pinning-dates:
runs-on: ubuntu-slim
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Validate JSON Schema
shell: pwsh
run: ./helpers/CheckOutdatedVersionPinning.ps1
env:
GH_TOKEN: ${{ github.token }}
================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '32 4 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
# only required for workflows in private repositories
actions: read
contents: read
# required for all workflows
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
================================================
FILE: .github/workflows/create_github_release.yml
================================================
name: Create GitHub release
on:
repository_dispatch:
types: [create-github-release]
jobs:
Create_GitHub_release:
runs-on: ubuntu-latest
steps:
- name: Create release for ${{ github.event.client_payload.ReleaseBranchName }}
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b #v1.20.0
with:
tag: ${{ github.event.client_payload.ReleaseBranchName }}
name: ${{ github.event.client_payload.ReleaseTitle }}
body: ${{ github.event.client_payload.ReleaseBody }}
prerelease: ${{ github.event.client_payload.Prerelease }}
commit: ${{ github.event.client_payload.Commitish }}
allowUpdates: true
================================================
FILE: .github/workflows/create_pull_request.yml
================================================
name: Create Pull Request
on:
repository_dispatch:
types: [create-pr]
jobs:
Create_pull_request:
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Clone release branch to create pull request
run: |
git checkout ${{ github.event.client_payload.ReleaseBranchName }}
git branch ${{ github.event.client_payload.ReleaseBranchName }}-docs
git push origin ${{ github.event.client_payload.ReleaseBranchName }}-docs --force
- name: Create pull request for ${{ github.event.client_payload.ReleaseBranchName }}
id: create-pr
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pulls = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:${{ github.event.client_payload.ReleaseBranchName }}-docs`,
base: "${{ github.event.client_payload.PullRequestBase }}",
state: 'open'
});
if (pulls.data.length > 0) {
console.log(`Pull request already exists: ${pulls.data[0].html_url}`);
return pulls.data[0].number;
} else {
console.log('No existing pull request found, creating new one');
let response = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: "${{ github.event.client_payload.PullRequestTitle }}",
head: "${{ github.event.client_payload.ReleaseBranchName }}-docs",
base: "${{ github.event.client_payload.PullRequestBase }}",
body: `${{ github.event.client_payload.PullRequestBody }}`
});
return response.data.number;
}
- name: Request reviewers
uses: actions/github-script@v8
with:
github-token: ${{secrets.PRAPPROVAL_SECRET}}
script: |
github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ steps.create-pr.outputs.result }},
team_reviewers: ['runner-images-team']
})
================================================
FILE: .github/workflows/create_sbom_report.yml
================================================
name: Create SBOM for the release
run-name: Collecting SBOM for ${{ github.event.client_payload.agentSpec || 'unknown image' }} - ${{ github.event.client_payload.imageVersion || 'unknown version' }}
on:
repository_dispatch:
types: [generate-sbom]
defaults:
run:
shell: pwsh
jobs:
sbom-check:
outputs:
check_status: ${{ steps.check.outputs.status }}
runs-on: ubuntu-latest
env:
RELEASE_ID: ${{ github.event.client_payload.ReleaseID }}
steps:
- name: Check SBOM asset for release ${{ env.RELEASE_ID }}
id: check
shell: pwsh
run: |
$apiUrl = "https://api.github.com/repos/actions/runner-images/releases/$env:RELEASE_ID"
$response = Invoke-RestMethod -Uri $apiUrl -Method Get -SkipHttpErrorCheck
if ($response.message -ilike "Not Found") {
echo "status=release_not_found" >> $env:GITHUB_OUTPUT
Write-Error "Release $env:RELEASE_ID wasn't found"
exit 1
}
foreach ($asset in $response.assets) {
if ($asset.name -like '*sbom*') {
echo "status=sbom_exists" >> $env:GITHUB_OUTPUT
return "Release $env:RELEASE_ID already contains a SBOM"
}
}
Write-Host "Release has been found, SBOM is not attached, starting generation."
echo "status=okay" >> $env:GITHUB_OUTPUT
building-sbom:
needs: sbom-check
if: ${{ needs.sbom-check.outputs.check_status == 'okay' }}
runs-on: ${{ github.event.client_payload.agentSpec }}
env:
AGENT_SPEC: ${{ github.event.client_payload.agentSpec }}
RELEASE_ID: ${{ github.event.client_payload.ReleaseID }}
IMAGE_VERSION: ${{ github.event.client_payload.imageVersion }}
steps:
- name: Available image version check
run: |
$expectedVersion = $env:IMAGE_VERSION
$runnerVersion = $env:ImageVersion
# Split versions by dot
$expectedParts = $expectedVersion.Split('.')
$runnerParts = $runnerVersion.Split('.')
# Determine what parts to compare
$minLength = [Math]::Min($expectedParts.Length, $runnerParts.Length)
$expectedComparable = $expectedParts[0..($minLength-1)] -join '.'
$runnerComparable = $runnerParts[0..($minLength-1)] -join '.'
# Perform the comparison
if ($expectedComparable -ne $runnerComparable) {
throw "Version mismatch: Expected version '$expectedVersion' doesn't match runner version '$runnerVersion'"
}
- name: Install SYFT tool on Windows
if: ${{ runner.os == 'Windows' }}
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b C:/syft
- name: Install SYFT tool on Ubuntu
if: ${{ runner.os == 'Linux' }}
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Install SYFT v1.24.0 on macOS
if: ${{ runner.os == 'macOS' }}
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin v1.24.0
- name: Run SYFT on Windows
if: ${{ runner.os == 'Windows' }}
run: C:/syft/syft dir:C:/ -vv -o spdx-json=sbom.json
- name: Run SYFT on Ubuntu
if: ${{ runner.os == 'Linux' }}
run: syft dir:/ -vv -o spdx-json=sbom.json
- name: Run SYFT on macOS
if: ${{ runner.os == 'macOS' }}
# Skip protected folders to avoid prompt privileges that block process indefinitely (https://github.com/anchore/syft/issues/1367)
run: sudo syft dir:/ -vv -o spdx-json=sbom.json --exclude ./Users --exclude ./System/Volumes --exclude ./private
shell: bash
- name: Compress SBOM file
run: Compress-Archive sbom.json sbom.json.zip
- uses: actions/upload-artifact@v4
with:
name: sbom-${{ env.AGENT_SPEC }}-${{ env.IMAGE_VERSION }}
path: sbom.json.zip
if-no-files-found: warn
- name: Upload release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: "https://uploads.github.com/repos/actions/runner-images/releases/${{ env.RELEASE_ID }}/assets{?name,label}"
asset_path: ./sbom.json.zip
asset_name: sbom.${{ env.AGENT_SPEC }}.json.zip
asset_content_type: application/zip
================================================
FILE: .github/workflows/docker-images.yml
================================================
name: Test Docker Images
on:
push:
branches:
- main
paths:
- 'images/ubuntu-slim/**'
- '.github/workflows/docker-images.yml'
pull_request:
paths:
- 'images/ubuntu-slim/**'
- '.github/workflows/docker-images.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
test-images:
runs-on: ubuntu-latest
strategy:
matrix:
directory:
- images/ubuntu-slim
steps:
- uses: actions/checkout@v6
- name: Run test.sh
working-directory: ${{ matrix.directory }}
run: ./test.sh
================================================
FILE: .github/workflows/linter.yml
================================================
# CI Validation
name: Linter
on:
pull_request:
branches: [ main ]
paths:
- '**.json'
- '**.md'
- '**.sh'
jobs:
build:
name: Lint JSON & MD files
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Lint Code Base
uses: github/super-linter/slim@v7
env:
VALIDATE_ALL_CODEBASE: false
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_JSON: true
VALIDATE_MARKDOWN: true
DEFAULT_BRANCH: ${{ github.base_ref }}
FILTER_REGEX_EXCLUDE: .*images/*/.*-Readme.md
- name: Checking shebang lines in MacOS and Ubuntu releases.
run: ./images.CI/shebang-linter.ps1
shell: pwsh
================================================
FILE: .github/workflows/merge_pull_request.yml
================================================
name: Merge pull request
on:
repository_dispatch:
types: [merge-pr]
jobs:
Merge_pull_request:
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Resolve possible conflicts ${{ github.event.client_payload.ReleaseBranchName }} with main
run: |
git config --global user.email "no-reply@github.com"
git config --global user.name "Actions service account"
git checkout ${{ github.event.client_payload.ReleaseBranchName }}-docs
git merge --no-edit --strategy-option=ours main
git push origin ${{ github.event.client_payload.ReleaseBranchName }}-docs
sleep 30
- name: Approve pull request by GitHub-Actions bot
uses: actions/github-script@v8
with:
github-token: ${{secrets.PRAPPROVAL_SECRET}}
script: |
github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.client_payload.PullRequestNumber }},
event: "APPROVE"
});
- name: Merge pull request for ${{ github.event.client_payload.ReleaseBranchName }}
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.client_payload.PullRequestNumber }},
merge_method: "squash"
})
================================================
FILE: .github/workflows/powershell-tests.yml
================================================
# CI Validation
name: PowerShell Tests
on:
pull_request:
branches: [ main ]
paths:
- 'helpers/software-report-base/**'
jobs:
powershell-tests:
name: PowerShell tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v5
- name: Run Software Report module tests
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Invoke-Pester -Output Detailed "helpers/software-report-base/tests"
================================================
FILE: .github/workflows/trigger-ubuntu-win-build.yml
================================================
name: Trigger Build workflow
on:
workflow_call:
inputs:
image_type:
required: true
type: string
defaults:
run:
shell: pwsh
jobs:
trigger-workflow:
runs-on: ubuntu-latest
outputs:
ci_workflow_run_id: ${{ steps.resolve.outputs.ci_workflow_run_id }}
ci_workflow_run_url: ${{ steps.resolve.outputs.ci_workflow_run_url }}
env:
CI_PR_TOKEN: ${{ secrets.CI_PR_TOKEN }}
PR_TITLE: ${{ github.event.pull_request.title }}
CI_REPO: ${{ vars.CI_REPO }}
steps:
- name: Checkout Code
uses: actions/checkout@v5
- name: Trigger Build workflow
run: |
Import-Module ./helpers/GitHubApi.psm1
$gitHubApi = Get-GithubApi -Repository "${env:CI_REPO}" -AccessToken "${env:CI_PR_TOKEN}"
$eventType = "trigger-${{ inputs.image_type }}-build"
[string] $prGuid = New-Guid
$clientPayload = @{
pr_title = "${env:PR_TITLE} - " + $prGuid
custom_repo = "${{ github.event.pull_request.head.repo.full_name }}"
custom_repo_commit_hash = "${{ github.event.pull_request.head.sha }}"
}
$gitHubApi.DispatchWorkflow($eventType, $clientPayload)
"PR_GUID=$prGuid" | Out-File -Append -FilePath $env:GITHUB_ENV
- name: Resolve Workflow Run ID
id: resolve
run: |
Import-Module ./helpers/GitHubApi.psm1
$gitHubApi = Get-GithubApi -Repository "${env:CI_REPO}" -AccessToken "${env:CI_PR_TOKEN}"
$workflowFileName = $("{0}.yml" -f "${{ inputs.image_type }}").ToLower()
$WorkflowSearchPattern = "${env:PR_GUID}"
# It might take a few minutes for the action to start
$attempt = 1
do {
$workflowRuns = $gitHubApi.GetWorkflowRuns($WorkflowFileName).workflow_runs
$workflowRunId = ($workflowRuns | Where-Object {$_.display_title -match $WorkflowSearchPattern}).id | Select-Object -First 1
if (-not ([string]::IsNullOrEmpty($workflowRunId))) {
$workflowRun = $gitHubApi.GetWorkflowRun($workflowRunId)
Write-Host "Found the workflow run with ID $workflowRunId on attempt $attempt. Workflow run link: $($workflowRun.html_url)"
"ci_workflow_run_id=$workflowRunId" | Out-File -Append -FilePath $env:GITHUB_OUTPUT
"ci_workflow_run_url=$($workflowRun.html_url)" | Out-File -Append -FilePath $env:GITHUB_OUTPUT
break
}
Write-Host "Workflow run for $WorkflowSearchPattern pattern not found on attempt $attempt."
$attempt += 1
Start-Sleep 30
} until ($attempt -eq 10)
if ([string]::IsNullOrEmpty($workflowRunId)) {
throw "Failed to find a workflow run for '$WorkflowSearchPattern'."
}
wait-completion:
runs-on: ubuntu-latest
needs: trigger-workflow
steps:
- name: Checkout Code
uses: actions/checkout@v5
- name: Wait for workflow completion
env:
CI_PR_TOKEN: ${{ secrets.CI_PR_TOKEN }}
CI_REPO: ${{ vars.CI_REPO }}
run: |
./helpers/WaitWorkflowCompletion.ps1 `
-WorkflowRunId "${{ needs.trigger-workflow.outputs.ci_workflow_run_id }}" `
-Repository "${env:CI_REPO}" `
-AccessToken "${env:CI_PR_TOKEN}"
- name: Add Summary
if: always()
run: |
"# Test Partner Image" >> $env:GITHUB_STEP_SUMMARY
"| Key | Value |" >> $env:GITHUB_STEP_SUMMARY
"| :-----------: | :--------: |" >> $env:GITHUB_STEP_SUMMARY
"| Workflow Run | [Link](${{ needs.trigger-workflow.outputs.ci_workflow_run_url }}) |" >> $env:GITHUB_STEP_SUMMARY
"| Workflow Result | $env:CI_WORKFLOW_RUN_RESULT |" >> $env:GITHUB_STEP_SUMMARY
" " >> $env:GITHUB_STEP_SUMMARY
cancel-workflow:
runs-on: ubuntu-latest
needs: [trigger-workflow, wait-completion]
if: cancelled()
steps:
- name: Checkout Code
uses: actions/checkout@v5
- name: Cancel workflow
env:
CI_PR_TOKEN: ${{ secrets.CI_PR_TOKEN }}
CI_REPO: ${{ vars.CI_REPO }}
run: |
Import-Module ./helpers/GitHubApi.psm1
$gitHubApi = Get-GithubApi -Repository "${env:CI_REPO}" -AccessToken "${env:CI_PR_TOKEN}"
$gitHubApi.CancelWorkflowRun("${{ needs.trigger-workflow.outputs.ci_workflow_run_id }}")
================================================
FILE: .github/workflows/ubuntu2204.yml
================================================
name: Trigger Ubuntu22.04 CI
run-name: Ubuntu22.04 - ${{ github.event.pull_request.title }}
on:
pull_request_target:
types: labeled
paths:
- 'images/ubuntu/**'
defaults:
run:
shell: pwsh
jobs:
Ubuntu_2204:
if: github.event.label.name == 'CI ubuntu-all' || github.event.label.name == 'CI ubuntu-2204'
uses: ./.github/workflows/trigger-ubuntu-win-build.yml
with:
image_type: 'ubuntu2204'
secrets: inherit
================================================
FILE: .github/workflows/ubuntu2404.yml
================================================
name: Trigger Ubuntu24.04 CI
run-name: Ubuntu24.04 - ${{ github.event.pull_request.title }}
on:
pull_request_target:
types: labeled
paths:
- 'images/ubuntu/**'
defaults:
run:
shell: pwsh
jobs:
Ubuntu_2404:
if: github.event.label.name == 'CI ubuntu-all' || github.event.label.name == 'CI ubuntu-2404'
uses: ./.github/workflows/trigger-ubuntu-win-build.yml
with:
image_type: 'ubuntu2404'
secrets: inherit
================================================
FILE: .github/workflows/update_github_release.yml
================================================
name: Update release
on:
repository_dispatch:
types: [update-github-release]
jobs:
Update_GitHub_release:
runs-on: ubuntu-slim
steps:
- name: Update release for ${{ github.event.client_payload.ReleaseBranchName }}
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const response = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: "${{ github.event.client_payload.ReleaseBranchName }}"
});
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: response.data.id,
prerelease: ${{ github.event.client_payload.Prerelease }}
});
================================================
FILE: .github/workflows/validate-json-schema.yml
================================================
name: Validate JSON Schema
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
validate-json-schema:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Validate JSON Schema
shell: pwsh
run: ./helpers/CheckJsonSchema.ps1
================================================
FILE: .github/workflows/windows2022.yml
================================================
name: Trigger Windows22 CI
run-name: Windows2022 - ${{ github.event.pull_request.title }}
on:
pull_request_target:
types: labeled
paths:
- 'images/windows/**'
defaults:
run:
shell: pwsh
jobs:
Windows_2022:
if: github.event.label.name == 'CI windows-all' || github.event.label.name == 'CI windows-2022'
uses: ./.github/workflows/trigger-ubuntu-win-build.yml
with:
image_type: 'windows2022'
secrets: inherit
================================================
FILE: .github/workflows/windows2025-vs2026.yml
================================================
name: Trigger Windows25 with VS 2026 CI
run-name: Windows2025 with VS 2026 - ${{ github.event.pull_request.title }}
on:
pull_request_target:
types: labeled
paths:
- 'images/windows/**'
defaults:
run:
shell: pwsh
jobs:
Windows_2025_vs_2026:
if: github.event.label.name == 'CI windows-all' || github.event.label.name == 'CI windows-2025-vs2026'
uses: ./.github/workflows/trigger-ubuntu-win-build.yml
with:
image_type: 'windows2025-vs2026'
secrets: inherit
================================================
FILE: .github/workflows/windows2025.yml
================================================
name: Trigger Windows25 CI
run-name: Windows2025 - ${{ github.event.pull_request.title }}
on:
pull_request_target:
types: labeled
paths:
- 'images/windows/**'
defaults:
run:
shell: pwsh
jobs:
Windows_2025:
if: github.event.label.name == 'CI windows-all' || github.event.label.name == 'CI windows-2025'
uses: ./.github/workflows/trigger-ubuntu-win-build.yml
with:
image_type: 'windows2025'
secrets: inherit
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# VSCode settings
.vscode/**
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/tasks.json
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# Ignore files generated by packer
InstalledSoftware.md
# Desktop Service Store
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# gatsby files
.cache/
public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# visual studio code launch configuration
launch.json
# Ignore dynamic template
images/*/*-temp.json
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"streetsidesoftware.code-spell-checker",
"hashicorp.hcl",
"davidanson.vscode-markdownlint",
"ms-vscode.powershell",
"timonwong.shellcheck"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"files.trimFinalNewlines": true,
"files.insertFinalNewline": true,
"powershell.codeFormatting.addWhitespaceAroundPipe": true,
"powershell.codeFormatting.alignPropertyValuePairs": true,
"powershell.codeFormatting.autoCorrectAliases": true,
"powershell.codeFormatting.newLineAfterCloseBrace": false,
"powershell.codeFormatting.newLineAfterOpenBrace": true,
"powershell.codeFormatting.openBraceOnSameLine": true,
"powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline",
"powershell.codeFormatting.preset": "OTBS",
"powershell.codeFormatting.trimWhitespaceAroundPipe": true,
"powershell.codeFormatting.whitespaceAfterSeparator": true,
"powershell.codeFormatting.whitespaceAroundOperator": true,
"powershell.codeFormatting.whitespaceBeforeOpenBrace": true,
"powershell.codeFormatting.whitespaceBeforeOpenParen": true,
"powershell.codeFormatting.whitespaceBetweenParameters": true,
"powershell.codeFormatting.whitespaceInsideBrace": true,
"shellcheck.exclude": [
"SC1090","SC2096"
],
"shellcheck.customArgs": [
"-x"
],
"json.schemas": [
{
"fileMatch": [
"**/toolset-*.json"
],
"url": "./schemas/toolset-schema.json"
}
]
}
================================================
FILE: .vscode/tasks.json
================================================
// Available variables which can be used inside of strings.
// ${workspaceRoot}: the root folder of the team
// ${file}: the current opened file
// ${relativeFile}: the current opened file relative to workspaceRoot
// ${fileBasename}: the current opened file's basename
// ${fileDirname}: the current opened file's dirname
// ${fileExtname}: the current opened file's extension
// ${cwd}: the current working directory of the spawned process
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
// Start PowerShell
"windows": {
"command": "${env:windir}/System32/WindowsPowerShell/v1.0/powershell.exe",
//"command": "${env:ProgramFiles}/PowerShell/6.0.0/powershell.exe",
"args": [ "-NoProfile", "-ExecutionPolicy", "Bypass" ]
},
"linux": {
"command": "/usr/bin/powershell",
"args": [ "-NoProfile" ]
},
"osx": {
"command": "/usr/local/bin/powershell",
"args": [ "-NoProfile" ]
},
// Associate with test task runner
"tasks": [
{
"taskName": "Test",
"suppressTaskName": true,
"isTestCommand": true,
"args": [
"Write-Host 'Invoking Pester...'; $ProgressPreference = 'SilentlyContinue'; Invoke-Pester -Script test -PesterOption @{IncludeVSCodeMarker=$true};",
"Invoke-Command { Write-Host 'Completed Test task in task runner.' }"
],
"problemMatcher": "$pester"
}
]
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at opensource@github.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
[fork]: https://github.com/actions/runner-images/fork
[pr]: https://github.com//actions/runner-images/compare
[code-of-conduct]: CODE_OF_CONDUCT.md
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [MIT](LICENSE.md) license.
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project, you agree to abide by its terms.
## Contents
- [Submitting a pull request](#submitting-a-pull-request)
- [Adding a new tool to an image](#adding-a-new-tool-to-an-image)
- [Code style guide](#code-style-guide)
## Submitting a pull request
1. [Fork][fork] and clone the repository.
1. Create a new branch: `git checkout -b my-branch-name`.
1. Make your changes, ensuring that they include steps to install, validate post-install, and update the software report (please see [Adding a new tool to an image](#adding-a-new-tool-to-an-image) for details).
1. Test your changes by [creating an image and deploying a VM](docs/create-image-and-azure-resources.md).
1. Push to your fork and [submit a pull request][pr].
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
- Follow the style guide for [Powershell](https://github.com/PoshCode/PowerShellPracticeAndStyle) when writing Windows scripts. There is currently no set style for the Shell scripts that run Linux installs :soon:.
- Include complete details of why this is needed in the PR description.
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write [good commit messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
- For new tools:
- Make sure that the tool satisfies the [Software Guidelines](README.md#software-guidelines).
- Create an issue and get approval from us to add this tool to the image before creating the pull request.
## Adding a new tool to an image
### General rules
- For every new tool, add validation scripts and update the software report script to ensure that it is included in the documentation.
- If the tool is available on multiple platforms (macOS, Windows, Linux), make sure you include it on as many as possible.
- If installing multiple versions of the tool, consider putting the list of versions in the corresponding `toolset.json` file. This will help other customers configure their builds flexibly. See [toolset-windows-2022.json](images/windows/toolsets/toolset-2022.json) as an example.
- Use consistent naming across all files.
- Validation scripts should be simple and shouldn't change the image content.
### Windows
- Add a script that will install the tool and put the script in the `scripts/build` folder.
There are a bunch of helper functions that could simplify your code: `Install-ChocoPackage`, `Install-Binary`, `Install-VSIXFromFile`, `Install-VSIXFromUrl`, `Invoke-DownloadWithRetry`, `Test-IsWin22`, `Test-IsWin25` (find the full list of helpers in [ImageHelpers.psm1](images/windows/scripts/helpers/ImageHelpers.psm1)).
- Add a script that will validate the tool installation and put the script in the `scripts/tests` folder.
We use [Pester v5](https://github.com/pester/pester) for validation scripts. If the tests for the tool are complex enough, create a separate `*.Tests.ps1`. Otherwise, use `Tools.Tests.ps1` for simple tests.
Add `Invoke-PesterTests -TestFile <testFileName> [-TestName <describeName>]` at the end of the installation script to ensure that your tests will be run.
- Add changes to the software report generator `images/windows/scripts/docs-gen/Generate-SoftwareReport.ps1`. The software report generator is used to generate an image's README file, e.g. [Windows2022-Readme.md](images/windows/Windows2022-Readme.md) and uses [MarkdownPS](https://github.com/Sarafian/MarkdownPS).
### Ubuntu
- Add a script that will install and validate the tool and put the script in the `scripts/build` folder.
Use existing scripts such as [github-cli.sh](images/ubuntu/scripts/build/github-cli.sh) as a starting point.
- Use [helpers](images/ubuntu/scripts/helpers/install.sh) to simplify the installation process.
- The validation part should `exit 1` if there is any issue with the installation.
- Add changes to the software report generator `images/ubuntu/scripts/docs-gen/Generate-SoftwareReport.ps1`. The software report generator is used to generate an image's README file, e.g. [Ubuntu2204-Readme.md](images/ubuntu/Ubuntu2204-Readme.md) and it uses [MarkdownPS](https://github.com/Sarafian/MarkdownPS).
### macOS
The macOS source lives in this repository and is available for everyone. However, the macOS image-generation CI doesn't support external contributions yet, so we are not able to accept pull requests for now.
We are in the process of preparing the macOS CI to accept contributions. Until then, we appreciate your patience and ask that you continue to make tool requests by filing issues.
## Code style guide
The principles of clean code apply to all languages. The main points are:
- Use meaningful names for variables, functions, files, etc.
- Keep functions short and simple.
- Use comments to explain what the code does.
- Use a consistent code style, naming convention, and file structure.
### File structure
- Each file should have a header with a title and a short description of the file.
- Each file should have a newline at the end.
- Use blank lines to separate logical blocks of code, but don't abuse blank lines:
- Don't add a blank line in the beginning and end of a block or function.
- Don't add blank lines between logically connected statements.
- Avoid trailing whitespace.
### Bash scripts
#### Naming convention for bash scripts
- Use lowercase letters for variable names.
- Use uppercase letters for constants.
- Use underscores to separate words in variable names.
#### Bash script structure
Each script should start with the following shebang:
```bash
#!/bin/bash -e
```
> TODO: do we need to set pipefail?
This will make the script exit if any command fails.
After the shebang, add a header with the following format:
```bash
################################################################################
## File: <filename>
## Desc: <short description of what the script does>
################################################################################
```
Then import helpers that are used in the script.
For Linux:
```bash
source $HELPER_SCRIPTS/os.sh
source $HELPER_SCRIPTS/install.sh
source $HELPER_SCRIPTS/etc-environment.sh
```
For macOS:
```bash
source ~/utils/utils.sh
```
> [!NOTE]
> You don't need to import all helpers, only the ones that are used in the script.
After that, add the script code.
### Indentations and line breaks in bash scripts
- Use 4 spaces for indentation.
- Use 1 space between `if`/`for`/`while` and `[[` and between `[[` and the condition.
- Place `then`/`do` on the new line.
- For short `if`/`for`/`while` statements, use the one-line format.
- Break long pipelines using `\`.
### Other recommendations for bash scripts
- For command substitution, use `$()` instead of backticks.
- Use `[[` instead of `[` for conditional expressions.
- Prefer using long options instead of short keys, but there are exceptions, e.g.:
- `tar -xzf`
- `apt-get -yqq`
- `curl -sSLf`
- `wget -qO-`
### PowerShell scripts
#### Naming convention for PowerShell scripts
- Use camelCase for variable names.
- Use uppercase letters for constants.
- Use `Verb-Noun` and PascalCase for function names.
### PowerShell script structure
Each script should start with the following header:
```powershell
################################################################################
## File: <filename>
## Desc: <short description of what the script does>
################################################################################
```
Then declare functions that are used in the script.
> TODO: do we need to set the error action preference and progress preference?
>
> ```powershell
> $ErrorActionPreference = "Stop"
> $ProgressPreference = "SilentlyContinue"
> ```
For Linux and macOS, import helpers that are used in the script:
For Linux:
```powershell
Import-Module "$env:HELPER_SCRIPTS/Tests.Helpers.psm1" -DisableNameChecking
```
For macOS:
```powershell
Import-Module "$env:HOME/image-generation/helpers/Common.Helpers.psm1"
Import-Module "$env:HOME/image-generation/helpers/Xcode.Helpers.psm1" -DisableNameChecking
```
> [!NOTE]
> You don't need to import all helpers, only the ones that are used in the script.
After that, add the script code.
### Indentations and line breaks in PowerShell scripts
- Use 4 spaces for indentation.
- Use 1 space between `if`/`elseif`/`foreach` and `(` but not between `(` and the condition.
- Add a space before and after pipe `|` and redirection `>` operators.
- Align properties in hash tables.
- Use [1TBS](https://en.wikipedia.org/wiki/Indentation_style#Variant:_1TBS_(OTBS)) style for curly braces:
- If block of statement is long, then place it on the new line, indent it, and add a closing curly brace on the new line.
- If block of statement is short, then place it on the same line as the statement.
```powershell
function Show-Example1 {
$exampleVariable = Get-ChildItem $env:TEMP
$exampleVariable | ForEach-Object {
$itemName = $_.Name
$itemPath = $_.FullName
}
}
$Example2 | Some-Function -Arguments @{Parameter1 = "Disabled"}
```
- Avoid using aliases.
- Break long pipelines using backticks or use [splatting](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting?view=powershell-7.3):
```powershell
# Instead of this
Copy-Item -Path "test.txt" -Destination "test2.txt" -WhatIf
# you can use this
$HashArguments = @{
Path = "test.txt"
Destination = "test2.txt"
WhatIf = $true
}
Copy-Item @HashArguments
```
When using backticks be extra careful with trailing whitespace as they can cause errors.
### Other recommendations for PowerShell scripts
- Verify exit codes of commands.
- When writing a function, provide a docstring that describes what the function does.
## Resources
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2026 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# GitHub Actions Runner Images
**Table of Contents**
- [About](#about)
- [Available Images](#available-images)
- [Announcements](#announcements)
- [Image Definitions](#image-definitions)
- [Image Releases](#image-releases)
- [Software and Image Support](#software-and-image-support)
- [How to Interact with the Repo](#how-to-interact-with-the-repo)
- [FAQs](#faqs)
## About
This repository contains the source code used to create the VM images for [GitHub-hosted runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners) used for Actions, as well as for [Microsoft-hosted agents](https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops#use-a-microsoft-hosted-agent) used for Azure Pipelines.
To build a VM machine from this repo's source, see the [instructions](docs/create-image-and-azure-resources.md).
## Available Images
| Image | Architecture | YAML Label | Included Software |
| --------------------|--------------|---------------------|------------------|
| Ubuntu 24.04<br> | x64 | `ubuntu-latest` or `ubuntu-24.04` | [ubuntu-24.04] |
| Ubuntu 22.04<br> | x64 | `ubuntu-22.04` | [ubuntu-22.04] |
| Ubuntu Slim <br> | x64 | `ubuntu-slim` | [ubuntu-slim] |
| macOS 26 Arm64<br> | arm64 | `macos-26` or `macos-26-xlarge` | [macOS-26-arm64] |
| macOS 26<br> | x64 | `macos-26-intel`, `macos-26-large` | [macOS-26] |
| macOS 15<br> | x64 | `macos-latest-large`, `macos-15-large`, or `macos-15-intel` | [macOS-15] |
| macOS 15 Arm64<br> | arm64 | `macos-latest`, `macos-15`, or `macos-15-xlarge` | [macOS-15-arm64] |
| macOS 14<br> | x64 | `macos-14-large`| [macOS-14] |
| macOS 14 Arm64<br> | arm64 | `macos-14` or `macos-14-xlarge`| [macOS-14-arm64] |
| Windows Server 2025 with Visual Studio 2026 <br> | x64 | `windows-2025-vs2026` | [windows-2025-vs2026] |
| Windows Server 2025<br> | x64 | `windows-latest` or `windows-2025` | [windows-2025] |
| Windows Server 2022<br> | x64 | `windows-2022` | [windows-2022] |
### Label scheme
- In general the `-latest` label is used for the latest OS image version that is GA.
- Before moving the `-latest` label to a new OS version we will announce the change and give sufficient lead time for users to update their workflows.
- The `-xlarge` and `-large` suffixes are unique to macOS images and are only available for GitHub Actions. Learn more about [GitHub Actions larger runners](https://docs.github.com/en/actions/reference/runners/larger-runners#available-macos-larger-runners-and-labels).
[ubuntu-24.04]: https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md
[ubuntu-22.04]: https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2204-Readme.md
[ubuntu-slim]: https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
[windows-2025]: https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-Readme.md
[windows-2025-vs2026]: https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-VS2026-Readme.md
[windows-2022]: https://github.com/actions/runner-images/blob/main/images/windows/Windows2022-Readme.md
[macOS-14]: https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md
[macOS-14-arm64]: https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md
[macOS-15]: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
[macOS-15-arm64]: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
[macOS-26]: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-Readme.md
[macOS-26-arm64]: https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
[self-hosted runners]: https://help.github.com/en/actions/hosting-your-own-runners
## Announcements
See notable upcoming changes by viewing issues with the [Announcement](https://github.com/actions/runner-images/labels/Announcement) label.
## Image Definitions
### Beta
The purpose of a Beta is to collect feedback on an image before it is released to GA. The goal of a Beta is to identify and fix any potential issues that exist on that
image. Images are updated on a weekly cadence. Any workflows that run on a beta image do not fall under the customer [SLA](https://github.com/customer-terms/github-online-services-sla) in place for Actions.
Customers choosing to use Beta images are encouraged to provide feedback in the runner-images repo by creating an issue. A Beta may take on different availability, i.e. public vs private.
### GA
A GA (General Availability) image has been through a Beta period and is deemed ready for general use. Images are updated on a weekly cadence. In order to be moved to
GA the image must meet the following criteria:
1. Has been through a Beta period (public or private)
2. Most major software we install on the image has a compatible
version for the underlying OS and
3. All major bugs reported during the Beta period have been addressed.
This image type falls under the customer [SLA](https://github.com/customer-terms/github-online-services-sla) for actions. GA images are eventually deprecated according to our guidelines as we only support the
latest 2 versions of an OS.
#### Latest Migration Process
GitHub Actions and Azure DevOps use the `-latest` YAML label (ex: `ubuntu-latest`, `windows-latest`, and `macos-latest`). These labels point towards the newest stable OS version available.
The `-latest` migration process is gradual and happens over 1-2 months in order to allow customers to adapt their workflows to the newest OS version. During this process, any workflow using the `-latest` label, may see changes in the OS version in their workflows or pipelines. To avoid unwanted migration, users can specify a specific OS version in the yaml file (ex: macos-14, windows-2022, ubuntu-22.04).
## Image Releases
*How to best follow along with changes*
1. Find the latest releases for this repository [here](https://github.com/actions/runner-images/releases).
2. Subscribe to the releases coming out of this repository, instructions [here](https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications#configuring-your-watch-settings-for-an-individual-repository).
3. Upcoming changes: A pre-release is created when the deployment of an image has started. As soon as the deployment is finished, the pre-release is converted to a release. If you have subscribed to releases, you will get notified of pre-releases as well.
- You can also track upcoming changes using the [awaiting-deployment](https://github.com/actions/runner-images/labels/awaiting-deployment) label.
4. For high impact changes, we will post these in advance to the GitHub Changelog on our [blog](https://github.blog/changelog/) and on [X](https://x.com/GHchangelog).
- Ex: breaking changes, GA or deprecation of images
*Cadence*
- We typically deploy weekly updates to the software on the runner images.
## Software and Image Support
### Support Policy
- Tools and versions will typically be removed 6 months after they are deprecated or have reached end-of-life
- We support (at maximum) 2 GA images and 1 beta image at a time. We begin the deprecation process of the oldest image label once the newest OS image label has been released to GA.
- The images generally contain the latest versions of packages installed except for Ubuntu LTS where we mostly rely on the Canonical-provided repositories.
- Popular tools can have several versions installed side-by-side with the following strategy:
| Tool name | Installation strategy |
|-----------|-----------------------|
| Docker images | not more than 3 latest LTS OS\tool versions. New images or new versions of current images are added using the standard tool request process |
| Java | all LTS versions |
| Node.js | 3 latest LTS versions |
| Go | 3 latest minor versions |
| Python <br/> Ruby | 5 most popular `major.minor` versions |
| PyPy | 3 most popular `major.minor` versions |
| .NET Core | 2 latest LTS versions and 1 latest version. For each feature version only latest patch is installed. Note for [Ubuntu images see details.](./docs/dotnet-ubuntu.md) |
| GCC <br/> GNU Fortran <br/> Clang <br/> GNU C++ | 3 latest major versions |
| Android NDK | 1 latest non-LTS, 2 latest LTS versions |
| Xcode | - only one major version of Xcode will be supported per macOS version <br/> - all minor versions of the supported major version will be available <br/> - beta and RC versions will be provided "as-is" in the latest available macOS image only no matter of beta/GA status of the image <br/> - when a new patch version is released, the previous patch version will be replaced |
| Xcode Platforms | - only three major.minor versions of platform tools and simulator runtimes will be available for installed Xcode, including beta/RC versions |
### Package managers usage
We use third-party package managers to install software during the image generation process. The table below lists the package managers and the software installed.
> [!NOTE]
> Third-party repositories are re-evaluated every year to identify if they are still useful and secure.
| Operating system | Package manager | Third-party repos and packages |
| :--- | :---: | ---: |
| Ubuntu | [APT](https://wiki.debian.org/Apt) | [docker](https://download.docker.com/linux/ubuntu) <br/> [Eclipse-Temurin (Adoptium)](https://packages.adoptium.net/artifactory/deb/) <br/> [Erlang](https://packages.erlang-solutions.com/ubuntu) <br/> [Firefox](https://ppa.launchpad.net/mozillateam/ppa/ubuntu) <br/> [git-lfs](https://packagecloud.io/install/repositories/github/git-lfs) <br/> [git](https://launchpad.net/~git-core/+archive/ubuntu/ppa) <br/> [Google Cloud CLI](https://packages.cloud.google.com/apt) <br/> [Heroku](https://cli-assets.heroku.com/channels/stable/apt) <br/> [HHvm](https://dl.hhvm.com/ubuntu) <br/> [MongoDB](https://repo.mongodb.org/apt/ubuntu) <br/> [Mono](https://download.mono-project.com/repo/ubuntu) <br/> [MS Edge](https://packages.microsoft.com/repos/edge) <br/> [PostgreSQL](https://apt.postgresql.org/pub/repos/apt/) <br/> [R](https://cloud.r-project.org/bin/linux/ubuntu) |
| | [pipx](https://pypa.github.io/pipx) | ansible-core <br/>yamllint |
| Windows | [Chocolatey](https://chocolatey.org) | No third-party repos installed |
| macOS | [Homebrew](https://brew.sh) | [aws-cli v2](https://github.com/aws/homebrew-tap) </br> [azure/bicep](https://github.com/Azure/homebrew-bicep) </br> [mongodb/brew](https://github.com/mongodb/homebrew-brew) |
| | [pipx](https://pypa.github.io/pipx/) | yamllint |
### Image Deprecation Policy
- Images begin the deprecation process of the oldest image label once a new GA OS version has been released.
- Deprecation process begins with an announcement that sets a date for deprecation.
- As it gets closer to the date, GitHub begins doing scheduled brownouts of the image.
- During this time there will be an Announcement pinned in the repo to remind users of the deprecation.
- Finally, GitHub will deprecate the image and it will no longer be available.
### Preinstallation Policy
In general, these are the guidelines we follow when deciding what to pre-install on our images:
- Popularity: widely-used tools and ecosystems will be given priority.
- Latest Technology: recent versions of tools will be given priority.
- Deprecation: end-of-life tools and versions will not be added.
- Licensing: MIT, Apache, or GNU licenses are allowed.
- Time & Space on the Image: we will evaluate how much time is saved and how much space is used by having the tool pre-installed.
- Support: If a tool requires the support of more than one version, we will consider the cost of this maintenance.
### Default Version Update Policy
- In general, once a new version is installed on the image, we announce the default version update 2 weeks prior to deploying it.
- For potentially dangerous updates, we may extend the timeline up to 1 month between the announcement and deployment.
## How to Interact with the Repo
- **Issues**: To file a bug report, or request tools to be added/updated, please [open an issue using the appropriate template](https://github.com/actions/runner-images/issues/new/choose)
- **Discussions**: If you want to share your thoughts about image configuration, installed software, or bring a new idea, please create a [new discussion](https://github.com/orgs/community/discussions/new?category=actions). Before making a new discussion, please make sure no similar topics were created earlier in the [actions category](https://github.com/orgs/community/discussions/categories/actions).
- For general questions about using the runner images or writing your Actions workflow, please open requests in the [GitHub Community discussion Actions category](https://github.com/orgs/community/discussions/categories/actions).
## FAQs
<details>
<summary><b><i>What images are available for GitHub Actions and Azure DevOps?</b></i></summary>
The availability of images for GitHub Actions and Azure DevOps is the same. However, deprecation policies may differ. See documentation for more details:
- [GitHub Actions](https://docs.github.com/en/free-pro-team@latest/actions/reference/specifications-for-github-hosted-runners#supported-runners-and-hardware-resources)
- [Azure DevOps](https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml#software)
</details>
<details>
<summary><b><i>What image version is used in my build?</b></i></summary>
Usually, image deployment takes 2-3 days, and documentation in the `main` branch is only updated when deployment is finished. To find out which image version and what software versions are used in a specific build, see `Set up job` (GitHub Actions) or `Initialize job` (Azure DevOps) step log.
<img width="1440" alt="actions-runner-image" src="https://github.com/actions/runner-images/assets/88318005/922a8bf5-3e4d-4265-9527-b3b51e6bf9c8">
</details>
<details>
<summary><b><i>Looking for other Linux distributions?</b></i></summary>
We do not plan to offer other Linux distributions. We recommend using Docker if you'd like to build using other distributions with the hosted runner images. Alternatively, you can leverage [self-hosted runners] and fully customize your VM image to your needs.
</details>
<details>
<summary><b><i>How do I contribute to the macOS source?</b></i></summary>
macOS source lives in this repository and is available for everyone. However, macOS image-generation CI doesn't support external contributions yet so we are not able to accept pull-requests for now.
We are in the process of preparing macOS CI to accept contributions. Until then, we appreciate your patience and ask you to continue to make tool requests by filing issues.
</details>
<details>
<summary><b><i>How does GitHub determine what tools are installed on the images?</b></i></summary>
For some tools, we always install the latest at the time of the deployment; for others, we pin the tool to specific version(s). For more details please see the [Preinstallation Policy](#preinstallation-policy)
</details>
<details>
<summary><b><i>How do I request that a new tool be pre-installed on the image?</b></i></summary>
Please create an issue and get an approval from us to add this tool to the image before creating the pull request.
</details>
<details>
<summary><b><i>What branch should I use to build custom image?</b></i></summary>
We strongly encourage customers to build their own images using the main branch.
This repository contains multiple branches and releases that serve as document milestones to reflect what software is installed in the images at certain point of time. Current builds are not idempotent and if one tries to build a runner image using the specific tag it is not guaranteed that the build will succeed.
</details>
================================================
FILE: SECURITY.md
================================================
If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github)
Thanks for helping make GitHub Actions safe for everyone.
================================================
FILE: docs/create-image-and-azure-resources.md
================================================
# GitHub Actions Runner Images
The runner-images project uses [Packer](https://www.packer.io/) to generate disk images for Windows 2022/2025 and Ubuntu 22.04/24.04.
Each image is configured by a HCL2 Packer template that specifies where to build the image (Azure, in this case),
and what steps to run to install software and prepare the disk.
The Packer process initializes a connection to the Azure subscription using Azure CLI and creates temporary resources
required for the build process: a resource group, network interfaces and a virtual machine from the "clean" image specified in the template.
If the VM deployment succeeds, Packer connects to it using SSH or WinRM and begins executing installation steps from the template one-by-one.
If any step fails, image generation is aborted, and the temporary VM is terminated.
Packer also attempts to clean up all the temporary resources it created (unless otherwise configured).
After successful completion of all installation steps, Packer creates a managed image from the temporary VM's disk and deletes the VM.
- [Build Agent Preparation](#build-agent-preparation)
- [Manual image generation](#manual-image-generation)
- [Manual Image Generation Customization](#manual-image-generation-customization)
- [Network Security](#network-security)
- [Azure Subscription Authentication](#azure-subscription-authentication)
- [Generated Machine Deployment](#generated-machine-deployment)
- [Automated image generation](#automated-image-generation)
- [Required variables](#required-variables)
- [Optional variables](#optional-variables)
- [Builder variables](#builder-variables)
- [Toolset](#toolset)
- [Post-generation scripts](#post-generation-scripts)
- [Running scripts](#running-scripts)
- [Script Details: Ubuntu](#script-details-ubuntu)
- [Script Details: Windows](#script-details-windows)
## Build Agent Preparation
The build agent is a machine where the Packer process will be started.
You can use any physical or virtual machine running Windows or Linux OS.
Of course, you may also use an [Azure VM](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/quick-create-cli).
In any case, you will need these software installed:
- Packer 1.8.2 or higher.
Download and install it manually from [here](https://www.packer.io/downloads) or use [Chocolatey](https://chocolatey.org/):
```powershell
choco install packer
```
- Git.
For Linux - install the latest version from your distro's package repo.
For Windows - download and install it from [here](https://gitforwindows.org/) or use [Chocolatey](https://chocolatey.org/):
```powershell
choco install git -params '"/GitAndUnixToolsOnPath"'
```
- Powershell 5.0 or higher.
In Windows you already have it.
For Linux follow instructions [here](https://learn.microsoft.com/en-us/windows-server/administration/linux-package-repository-for-microsoft-software)
to add Microsoft's Linux Software Repository and then install the `powershell` package.
- Azure CLI.
Follow the instructions [here](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli).
Or if you use Windows, you may run this command in Powershell instead:
```powershell
Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi
Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'; rm .\AzureCLI.msi
```
## Manual image generation
This repository includes a script that assists in generating images in Azure.
All you need is an Azure subscription, a resource group in that subscription and a build agent configured as described above.
All the commands below should be executed in PowerShell.
First, clone the runner-images repository and set the current directory to it:
```powershell
git clone https://github.com/actions/runner-images.git
Set-Location runner-images
```
Then, import the [GenerateResourcesAndImage](../helpers/GenerateResourcesAndImage.ps1) script from the `helpers` subdirectory:
```powershell
Import-Module .\helpers\GenerateResourcesAndImage.ps1
```
Finally, run the `GenerateResourcesAndImage` function, setting the mandatory arguments: image type and where to build and store the resulting managed image:
- `SubscriptionId` - your Azure Subscription ID;
- `ResourceGroupName` - the name of the resource group that will store the resulting artifact (e.g., "imagegen-test").
The resource group must already exist in your Azure subscription;
- `AzureLocation` - the location where resources will be created (e.g., "East US");
- `ImageType` - the type of image to build (valid options are "Windows2022", "Windows2025", "Ubuntu2204", "Ubuntu2404").
This function automatically creates all required Azure resources and initiates the Packer image generation for the selected image type.
When the image is ready, you may proceed to [deployment](#generated-machine-deployment).
## Manual Image Generation Customization
The `GenerateResourcesAndImage` function accepts a number of arguments that may assist you in generating an image in your specific environment.
For example, you may want all the resources involved in the image generation process to be tagged.
In this case, pass a HashTable of tags as a value for the `Tags` parameter.
If you don't want the function to authenticate interactively, you should create a Service Principal and invoke the function with the parameters `AzureClientId`, `AzureClientSecret` and `AzureTenantId`.
You can find more details in the [corresponding section below](#azure-subscription-authentication).
Use `get-help GenerateResourcesAndImage -Detailed` for the complete list of available parameters.
### Network Security
To connect to a temporary virtual machine, Packer uses WinRM or SSH.
If your build agent is located outside of the Azure subscription where the temporary VM is created, a public network interface and public IP address are used.
Make sure that firewalls are configured properly and that WinRM (TCP port 5986) and SSH (TCP port 22) connections are allowed both outgoing for the build agent and incoming for the temporary VM.
Also, if you don't want the temporary VM to be accessible from everywhere, set the `RestrictToAgentIpAddress` parameter value to `$true`
to set up firewall rules allowing access only from your build agent's public IP address.
If your build agent and temporary VM are in the same subscription, you can configure Packer to connect using a private virtual network.
To achieve this, set proper values for the environment variables `VNET_RESOURCE_GROUP`, `VNET_NAME` and `VNET_SUBNET`.
### Azure Subscription Authentication
Packer uses a Service Principal to authenticate in Azure infrastructure.
For more information about Service Principals, refer to the
[Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal).
The `GenerateResourcesAndImage` function is able to create a Service Principal to be used by Packer.
It uses the Connect-AzAccount cmdlet that invokes an interactive authentication process by default.
If you don't want to use interactive authentication, you should create a Service Principal with full read-write permissions for the selected Azure subscription on your own
and provide proper values for the parameters `AzureClientId`, `AzureClientSecret` and `AzureTenantId`.
Here is an example of how to create a Service Principal using the Az PowerShell module:
```powershell
$credentials = [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphPasswordCredential]@{
StartDateTime = Get-Date
EndDateTime = (Get-Date).AddDays(7)
}
$sp = New-AzADServicePrincipal -DisplayName "imagegen-app"
$appCred = New-AzADAppCredential -ApplicationId $sp.AppId -PasswordCredentials $credentials
Start-Sleep -Seconds 30
New-AzRoleAssignment -RoleDefinitionName "Contributor" -PrincipalId $sp.Id
Start-Sleep -Seconds 30
@{
ClientId = $sp.AppId
ClientSecret = $appCred.SecretText
TenantId = (Get-AzSubscription -SubscriptionId $SubscriptionId).TenantId
}
```
## Generated Machine Deployment
After successful image generation, a Virtual Machine can be created from the generated image using the [CreateAzureVMFromPackerTemplate](../helpers/CreateAzureVMFromPackerTemplate.ps1) script.
```powershell
Import-Module .\helpers\CreateAzureVMFromPackerTemplate.ps1
CreateAzureVMFromPackerTemplate -SubscriptionId {YourSubscriptionId} -ResourceGroupName {ResourceGroupName} -ManagedImageName "Runner-Image-Ubuntu2204" -VirtualMachineName "testvm1" -AdminUsername "shady1" -AdminPassword "SomeSecurePassword1" -AzureLocation "eastus"
```
Where:
- `SubscriptionId` - the Azure subscription ID where resources will be created;
- `ResourceGroupName` - the Azure resource group name where the Azure virtual machine will be created;
- `ManagedImageName` - the name of the managed image to be used for the virtual machine creation;
- `VirtualMachineName` - the name of the virtual machine to be generated;
- `AdminUserName` - the administrator username for the virtual machine to be created;
- `AdminPassword` - the administrator password for the virtual machine to be created;
- `AzureLocation` - the location where the Azure virtual machine will be provisioned (e.g., "eastus").
This function creates an Azure VM and generates network resources in Azure to make the VM accessible.
## Automated image generation
If you want to generate images automatically (e.g., as a part of a CI/CD pipeline),
you can use Packer directly. To do this, you will need:
- a build agent configured as described in the
[Build agent preparation](#build-agent-preparation) section;
- an Azure subscription and Service Principal configured as described in the
[Azure subscription authentication](#azure-subscription-authentication) section;
- a resource group created in your Azure subscription where the managed image will be stored;
- a string to be used as a password for the user used to install software (Windows only).
Then, you can invoke Packer in your CI/CD pipeline using the following commands:
```powershell
packer plugins install github.com/hashicorp/azure 2.2.1
packer build -only "$BuildName*" `
-var "subscription_id=$SubscriptionId" `
-var "client_id=$ClientId" `
-var "client_secret=$ClientSecret" `
-var "install_password=$InstallPassword" `
-var "location=$Location" `
-var "image_os=$ImageOS" `
-var "managed_image_name=$ImageName" `
-var "managed_image_resource_group_name=$ImageResourceGroupName" `
-var "tenant_id=$TenantId" `
$TemplatePath
```
Where:
- `BuildName` - name of the build defined in Packer template's `build{}` block (e.g. "ubuntu-24_04", "windows-2025");
- `SubscriptionId` - your Azure Subscription ID;
- `ClientId` and `ClientSecret` - Service Principal credentials;
- `TenantId` - Azure Tenant ID;
- `InstallPassword` - password for the user used to install software (Windows only);
- `Location` - location where resources will be created (e.g., "East US");
- `ImageOS` - the type of OS that will be deployed as a temporary VM (e.g. "ubuntu24", "win25");
- `ImageName` and `ImageResourceGroupName` - name of the resource group where the managed image will be stored;
- `TemplatePath` - path to the folder with Packer template files (e.g., "images/windows/templates").
### Required variables
The following variables are required to be passed to the Packer process:
| Template var | Env var | Description
| ------------ | ------- | -----------
| `subscription_id` | `ARM_SUBSCRIPTION_ID` | The subscription under which the build will be performed.
| `client_id` | `ARM_CLIENT_ID` | The Active Directory service principal associated with your builder.
| `client_secret` | `ARM_CLIENT_SECRET` | The password or secret for your service principal; may be omitted if `client_cert_path` is set.
| `client_cert_path` | `ARM_CLIENT_CERT_PATH` | The location of a PEM file containing a certificate and private key for the service principal; may be omitted if `client_secret` is set.
| `location` | `ARM_RESOURCE_LOCATION` | The Azure datacenter in which your VM will be built.
| `managed_image_resource_group_name` | `ARM_RESOURCE_GROUP` | The resource group under which the final artifact will be stored.
### Optional variables
The following variables are optional:
- `managed_image_name` - the name of the managed image to create. If not specified, "Runner-Image-{{ImageType}}" will be used;
- `build_resource_group_name` - specify an existing resource group to run the build in; by default, a temporary resource group will be created and destroyed as part of the build; if you do not have permission to do so, use `build_resource_group_name` to specify an existing resource group to run the build in;
- `object_id` - the object ID for the AAD SP; will be derived from the oAuth token if empty;
- `tenant_id` - the Active Directory tenant identifier with which your `client_id` and `subscription_id` are associated; if not specified, `tenant_id` will be looked up using `subscription_id`;
- `temp_resource_group_name` - the name assigned to the temporary resource group created during the build; if this value is not set, a random value will be assigned; this resource group is deleted at the end of the build;
- `private_virtual_network_with_public_ip` - this value allows you to set a `virtual_network_name` and obtain a public IP; if this value is not set and `virtual_network_name` is defined, Packer is only allowed to be executed from a host on the same subnet / virtual network;
- `virtual_network_name` - use a pre-existing virtual network for the VM; this option enables private communication with the VM, no public IP address is used or provisioned (unless you set `private_virtual_network_with_public_ip`);
- `virtual_network_resource_group_name` - if `virtual_network_name` is set, this value may also be set; if `virtual_network_name` is set, and this value is not set, the builder attempts to determine the resource group containing the virtual network; if the resource group cannot be found, or it cannot be disambiguated, this value should be set;
- `virtual_network_subnet_name` - if `virtual_network_name` is set, this value may also be set; if `virtual_network_name` is set, and this value is not set, the builder attempts to determine the subnet to use with the virtual network; if the subnet cannot be found, or it cannot be disambiguated, this value should be set.
## Builder variables
The `builders` section contains variables for the `azure-arm` builder used in the project. Most of the builder variables are inherited from the `user variables` section, however, the variables can be overwritten to adjust image-generation performance.
- `vm_size` - the size of the VM used for building; this can be changed when you deploy a VM from your image;
- `image_os` - the type of OS that will be deployed as a temporary VM;
- `image_version` - specify the version of an OS to boot from.
**Detailed Azure builders documentation can be found in the [packer documentation](https://www.packer.io/docs/builders/azure).**
## Toolset
The configuration for some installed software is located in `toolset.json` files. These files define the list of Ruby, Python, Go versions, the list of PowerShell modules and VS components that will be installed on the image. They can be changed if these tools are not required, to reduce image generation time or image size.
Generated tool versions and details can be found in related projects:
- [Python](https://github.com/actions/python-versions/)
- [Go](https://github.com/actions/go-versions)
- [Node](https://github.com/actions/node-versions)
## Post-generation scripts
> :warning: These scripts are intended to be run on a VM deployed in Azure
The user, created during the image generation, does not exist in the resulting image. Hence, some configuration files related to the user's home directory need to be changed, as well as the file permissions for some directories. Scripts for that are located in the `post-gen` folder in the repository:
- Windows: <https://github.com/actions/runner-images/tree/main/images/windows/assets/post-gen>
- Linux: <https://github.com/actions/runner-images/tree/main/images/ubuntu/assets/post-gen>
**Note:** The default user for Linux should have `sudo privileges`.
The scripts are copied to the image during the generation process to the following paths:
- Windows: `C:\post-generation`
- Linux: `/opt/post-generation`
### Running scripts
- Ubuntu
```bash
sudo su -c "find /opt/post-generation -mindepth 1 -maxdepth 1 -type f -name '*.sh' -exec bash {} \;"
```
- Windows
```powershell
Get-ChildItem C:\post-generation -Filter *.ps1 | ForEach-Object { & $_.FullName }
```
### Script Details: Ubuntu
- **cleanup-logs.sh** - removes all build process logs from the machine;
- **environment-variables.sh** - replaces `$HOME` with the default user's home directory for environment variables related to the default user home directory;
- **homebrew-permissions.sh** - resets the Homebrew repository directory by running `git reset --hard` to make the working tree clean after changing permissions in /home and changes the repository directory owner to the current user;
- **rust-permissions.sh** - fixes permissions for the Rust folder; a detailed issue explanation is provided in [runner-images/issues/572](https://github.com/actions/runner-images/issues/572).
### Script Details: Windows
- **GenerateIISExpressCertificate.ps1** - generates and imports a certificate to run applications with IIS Express through HTTPS;
- **InternetExplorerConfiguration.ps1** - turns off the Internet Explorer Enhanced Security feature;
- **Msys2FirstLaunch.ps1** - initializes the bash user profile in MSYS2;
- **VSConfiguration.ps1** - performs initial Visual Studio configuration.
================================================
FILE: docs/dotnet-ubuntu.md
================================================
# Ubuntu .NET Core Versions
.NET has changed the recommended install methods for Ubuntu from 2404.
This document gives an overview of these change and the impact this has on the `runner-images`.
## .NET Core for Ubuntu 2204
2204 uses the [Microsoft Package repository](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu-install?tabs=dotnet8&pivots=os-linux-ubuntu-2204) to install .NET deb files built and published by the .NET team.
## .NET Core Versions from Ubuntu 2404
The .NET Core team have worked with Canonical and Ubuntu now provides its own .NET packages.
These are the recommended install path and, as-such what is installed on the image.
> The release of Ubuntu 24.04 is just around the corner. Canonical-produced .NET 6, 7, and 8 packages will be available on day one, for "Noble Numbat". Microsoft will not be publishing .NET packages to the 24.04 feed at packages.microsoft.com.
You can read the [full announcement from .NET team here](https://github.com/dotnet/core/discussions/9258). We'll briefly summarize how this change may impact users of the image.
### [`Feature Bands`](https://learn.microsoft.com/dotnet/core/porting/versioning-sdk-msbuild-vs)
Going forward only the `1xx` feature band will be present in the image as Ubuntu only build and publish this band.
> Most distros, including Ubuntu, stick to the .1xx feature band for the lifetime of a major .NET version. They make this choice because .1xx is (effectively) the "compatibility band". Higher bands can have breaking changes.
> This means there will no longer be packages available for .2xx and later feature bands. Such packages have been exclusively available from Microsoft. If users see an incompatibility between .1xx and higher feature bands, we ask that you please report it in the dotnet/sdk repo. [link: dotnet/core discussion](https://github.com/dotnet/core/discussions/9258)
If you need a higher feature band for your Actions the recommendation is to use the [`setup-dotnet`](https://github.com/actions/setup-dotnet) action to install the desired version.
### .NET MAUI
.NET MAUI is [not included](https://github.com/dotnet/core/discussions/9258#discussioncomment-9548857) in the Ubuntu .NET package. There is work [ongoing to fix.](https://github.com/dotnet/core/discussions/9258#discussioncomment-9548857)
You should be able to resolve this by using the [`setup-dotnet`](https://github.com/actions/setup-dotnet) action to install the desired version.
================================================
FILE: helpers/CheckJsonSchema.ps1
================================================
$ErrorActionPreference = 'Stop'
# A JSON schema validator which supports outputting line numbers for errors
# this allows us to put annotations on builds for errors in the JSON files
# `Test-Json` built in cmdline doesn't. No existing cli tool supports this
# that I could find either. See: https://github.com/lawrencegripper/gripdev-json-schema-validator
Install-Module -Name GripDevJsonSchemaValidator -Force -Scope CurrentUser
# Find all toolset JSON files
$toolsetFiles = Get-ChildItem -Recurse -Filter "toolset-*.json" | Where-Object { $_.Name -notlike "*schema.json" }
$schemaFilePath = "./schemas/toolset-schema.json"
$toolsetHasErrors = $false
foreach ($file in $toolsetFiles) {
Write-Host ""
Write-Host "🔍 Validating $($file.FullName)" -ForegroundColor Cyan
$validationResult = Test-JsonSchema -SchemaPath $schemaFilePath -JsonPath $file.FullName -PrettyPrint $false
if ($validationResult.Valid) {
Write-Host "✅ JSON is valid." -ForegroundColor Green
} else {
# File has been modified since the commit, enforce validation
$toolsetHasErrors = $true
Write-Host "`n❌ JSON validation failed!" -ForegroundColor Red
Write-Host " Found the following errors:`n" -ForegroundColor Yellow
$validationResult.Errors | ForEach-Object {
Write-Host $_.UserMessage
if ($env:GITHUB_ACTIONS -eq 'true') {
Write-Host "Adding annotation"
Write-Host "::error file=$($file.Name),line=$($_.LineNumber)::$($_.UserMessage.Replace("`n", '%0A'))"
}
}
}
}
if ($toolsetHasErrors) {
Write-Error "One or more toolset JSON files failed schema validation. See the error output above for more details."
} else {
Write-Host "Schema validation completed successfully"
}
================================================
FILE: helpers/CheckOutdatedVersionPinning.ps1
================================================
$ErrorActionPreference = 'Stop'
# Find all toolset JSON files
$toolsetFiles = Get-ChildItem -Recurse -Filter "toolset-*.json" | Where-Object { $_.Name -notlike "*schema.json" }
$expiringPins = @()
$now = Get-Date
$warningDays = 30 # Warn if expiring within 30 days
foreach ($file in $toolsetFiles) {
Write-Host "Processing $($file.Name)"
$content = Get-Content $file.FullName | ConvertFrom-Json
# Recursively search for pinnedDetails in the JSON
function Search-PinnedDetails {
param($obj, $path)
$foundPins = @()
if ($obj -is [System.Management.Automation.PSCustomObject]) {
foreach ($prop in $obj.PSObject.Properties) {
if ($prop.Name -eq "pinnedDetails") {
Write-Host "Found pinned version at $path"
$reviewAt = [DateTime]::Parse($prop.Value.'review-at')
$daysUntilExpiry = ($reviewAt - $now).Days
if ($daysUntilExpiry -lt $warningDays) {
Write-Host "Adding to expiringPins array"
$foundPins += @{
Path = $path
File = $file.Name
ReviewAt = $reviewAt
DaysUntilExpiry = $daysUntilExpiry
Reason = $prop.Value.reason
Link = $prop.Value.link
}
}
} else {
$foundPins += Search-PinnedDetails -obj $prop.Value -path "$path.$($prop.Name)"
}
}
} elseif ($obj -is [Array]) {
for ($i = 0; $i -lt $obj.Count; $i++) {
$foundPins += Search-PinnedDetails -obj $obj[$i] -path "$path[$i]"
}
}
return $foundPins
}
$expiringPins += Search-PinnedDetails -obj $content -path $file.Name
}
if ($expiringPins) {
$issueBody = "# Version Pinning Review Required`n`n"
$issueBody += "The following pinned versions need review:`n`n"
foreach ($pin in $expiringPins) {
$status = if ($pin.DaysUntilExpiry -lt 0) { "EXPIRED" } else { "Expiring Soon" }
$issueBody += "## $($status) - $($pin.Path)`n"
$issueBody += "- **File**: $($pin.File)`n"
$issueBody += "- **Review Date**: $($pin.ReviewAt.ToString('yyyy-MM-dd'))`n"
$issueBody += "- **Days until expiry**: $($pin.DaysUntilExpiry)`n"
$issueBody += "- **Reason**: $($pin.Reason)`n"
$issueBody += "- **Original PR**: $($pin.Link)`n`n"
}
if ($env:GITHUB_ACTIONS -eq 'true') {
# In GitHub Actions, create an issue
Write-Host "Creating issue"
$tempFile = [System.IO.Path]::GetTempFileName()
Set-Content -Path $tempFile -Value $issueBody
gh issue create --title "Version Pinning Review Found Expired Pinned Versions" --body-file $tempFile
Remove-Item -Path $tempFile
}
Write-Host "`nIssue Content:`n"
Write-Host $issueBody
}
else {
Write-Host "No expiring pins found."
if ($env:GITHUB_ACTIONS -eq 'true') {
"expired_pins=0" >> $env:GITHUB_OUTPUT
}
}
================================================
FILE: helpers/CreateAzureVMFromPackerTemplate.ps1
================================================
Function CreateAzureVMFromPackerTemplate {
<#
.SYNOPSIS
A helper function to deploy a VM from a generated image.
.DESCRIPTION
Creates an Azure VM from a template. Also generates network resources in Azure to make the VM accessible.
.PARAMETER SubscriptionId
The Azure subscription Id where resources will be created.
.PARAMETER ResourceGroupName
The Azure resource group name where the Azure virtual machine will be created.
.PARAMETER ManagedImageName
The name of the managed image to be used to create the virtual machine.
.PARAMETER VirtualMachineName
The name of the virtual machine to be generated.
.PARAMETER AdminUserName
The administrator username for the virtual machine to be created.
.PARAMETER AdminPassword
The administrator password for the virtual machine to be created.
.PARAMETER AzureLocation
The location where the Azure virtual machine will be provisioned. Example: "eastus"
.EXAMPLE
CreateAzureVMFromPackerTemplate -SubscriptionId {SubscriptionId} -ResourceGroupName {ResourceGroupName} -VirtualMachineName "testvm1" -ManagedImageName {ManagedImageName} -AdminUsername "shady1" -AdminPassword "SomeSecurePassword1" -AzureLocation "eastus"
#>
param (
[Parameter(Mandatory = $True)]
[string] $SubscriptionId,
[Parameter(Mandatory = $True)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $True)]
[string] $ManagedImageName,
[Parameter(Mandatory = $True)]
[string] $VirtualMachineName,
[Parameter(Mandatory = $True)]
[string] $AdminUsername,
[Parameter(Mandatory = $True)]
[string] $AdminPassword,
[Parameter(Mandatory = $True)]
[string] $AzureLocation
)
$vmSize = "Standard_DS2_v2"
$guid = [System.GUID]::NewGuid().ToString().ToUpper()
$vnetName = $env:UserName + "vnet-" + $guid
$subnetName = $env:UserName + "subnet-" + $guid
$nicName = $env:UserName + "nic-" + $guid
$publicIpName = $env:UserName + "pip-" + $guid
Write-Host "Creating a virtual network and subnet"
($vnet = az network vnet create -g $ResourceGroupName -l $AzureLocation -n $vnetName --address-prefixes 10.0.0.0/16 --subnet-name $subnetName --subnet-prefixes 10.0.1.0/24 --subscription $subscriptionId -o json)
$subnetId = ($vnet | ConvertFrom-Json).newVNet.subnets[0].id
Write-Host "`nCreating a network interface controller (NIC)"
($nic = az network nic create -g $ResourceGroupName -l $AzureLocation -n $nicName --subnet $subnetId --subscription $subscriptionId -o json)
$networkId = ($nic | ConvertFrom-Json).NewNIC.id
Write-Host "`nCreating a public IP address"
($publicIp = az network public-ip create -g $ResourceGroupName -l $AzureLocation -n $publicIpName --allocation-method Static --sku Basic --version IPv4 --subscription $subscriptionId -o json)
$publicIpId = ($publicIp | ConvertFrom-Json).publicIp.id
Write-Host "`nAdding the public IP to the NIC"
az network nic ip-config update -g $ResourceGroupName -n ipconfig1 --nic-name $nicName --public-ip-address $publicIpId --subscription $subscriptionId
Write-Host "`nCreating the VM"
az vm create `
--resource-group $ResourceGroupName `
--name $VirtualMachineName `
--image $ManagedImageName `
--size $vmSize `
--admin-username $AdminUsername `
--admin-password $AdminPassword `
--nics $networkId `
--subscription $subscriptionId `
--location $AzureLocation
Write-Host "`nCreated in ${ResourceGroupName}:`n vnet ${vnetName}`n subnet ${subnetName}`n nic ${nicName}`n publicip ${publicIpName}`n vm ${VirtualMachineName}"
}
================================================
FILE: helpers/GenerateResourcesAndImage.ps1
================================================
$ErrorActionPreference = 'Stop'
enum ImageType {
Windows2022 = 1
Windows2025 = 2
Windows2025_vs2026 = 3
Ubuntu2204 = 4
Ubuntu2404 = 5
}
Function Get-PackerTemplate {
param (
[Parameter(Mandatory = $True)]
[string] $RepositoryRoot,
[Parameter(Mandatory = $True)]
[ImageType] $ImageType
)
switch ($ImageType) {
# Note: Double Join-Path is required to support PowerShell 5.1
([ImageType]::Windows2022) {
$relativeTemplatePath = Join-Path (Join-Path "windows" "templates") "build.windows-2022.pkr.hcl"
$imageOS = "win22"
}
([ImageType]::Windows2025) {
$relativeTemplatePath = Join-Path (Join-Path "windows" "templates") "build.windows-2025.pkr.hcl"
$imageOS = "win25"
}
([ImageType]::Windows2025_vs2026) {
$relativeTemplatePath = Join-Path (Join-Path "windows" "templates") "build.windows-2025-vs2026.pkr.hcl"
$imageOS = "win25-vs2026"
}
([ImageType]::Ubuntu2204) {
$relativeTemplatePath = Join-Path (Join-Path "ubuntu" "templates") "build.ubuntu-22_04.pkr.hcl"
$imageOS = "ubuntu22"
}
([ImageType]::Ubuntu2404) {
$relativeTemplatePath = Join-Path (Join-Path "ubuntu" "templates") "build.ubuntu-24_04.pkr.hcl"
$imageOS = "ubuntu24"
}
default { throw "Unknown type of image" }
}
$imageTemplatePath = [IO.Path]::Combine($RepositoryRoot, "images", $relativeTemplatePath)
# Specific template selection using Packer's "-only" functionality
$buildName = [IO.Path]::GetFileName($imageTemplatePath).Split(".")[1]
if (-not (Test-Path $imageTemplatePath)) {
throw "Template for image '$ImageType' doesn't exist on path '$imageTemplatePath'."
}
return [PSCustomObject] @{
"BuildName" = $buildName
"ImageOS" = $imageOS
"Path" = [IO.Path]::GetDirectoryName($imageTemplatePath)
}
}
Function Show-LatestCommit {
[CmdletBinding()]
param()
process {
$latestCommit = (git --no-pager log --pretty=format:"Date: %cd; Commit: %H - %s; Author: %an <%ae>" -1)
Write-Host "Latest commit: $latestCommit."
}
}
function Get-GitHubActionsOidcIdToken {
[CmdletBinding()]
param(
[Parameter(Mandatory = $True)]
[string] $RequestUrl,
[Parameter(Mandatory = $True)]
[string] $RequestToken,
[Parameter(Mandatory = $False)]
[string] $Audience = 'api://AzureADTokenExchange'
)
$separator = if ($RequestUrl -match '\?') { '&' } else { '?' }
$urlWithAudience = "${RequestUrl}${separator}audience=$([System.Uri]::EscapeDataString($Audience))"
$headers = @{ Authorization = "Bearer $RequestToken" }
try {
$response = Invoke-RestMethod -Method Get -Uri $urlWithAudience -Headers $headers
}
catch {
throw "Failed to request GitHub Actions OIDC ID token. Ensure workflow permissions include 'id-token: write'. Details: $($_.Exception.Message)"
}
if ([string]::IsNullOrEmpty($response.value)) {
throw "GitHub Actions OIDC token response did not contain a 'value' field."
}
return $response.value
}
function Start-Sleep($seconds) {
$doneDT = (Get-Date).AddSeconds($seconds)
while ($doneDT -gt (Get-Date)) {
$secondsLeft = $doneDT.Subtract((Get-Date)).TotalSeconds
$percent = ($seconds - $secondsLeft) / $seconds * 100
Write-Progress -Activity "Sleeping" -Status "Sleeping..." -SecondsRemaining $secondsLeft -PercentComplete $percent
[System.Threading.Thread]::Sleep(500)
}
Write-Progress -Activity "Sleeping" -Status "Sleeping..." -SecondsRemaining 0 -Completed
}
Function GenerateResourcesAndImage {
<#
.SYNOPSIS
A helper function to help generate an image.
.DESCRIPTION
This function will generate the Azure resources and image for the specified image type.
.PARAMETER SubscriptionId
The Azure subscription id where the Azure resources will be created.
.PARAMETER ResourceGroupName
The name of the resource group to store the resulting artifact. Resource group must already exist.
.PARAMETER ImageType
The type of image to generate. Valid values are: Windows2022, Windows2025, Windows2025_vs2026, Ubuntu2204, Ubuntu2404.
.PARAMETER ManagedImageName
The name of the managed image to create. The default is "Runner-Image-{{ImageType}}".
.PARAMETER AzureLocation
The Azure location where the Azure resources will be created. For example: "East US"
.PARAMETER ImageGenerationRepositoryRoot
The root directory of the image generation repository. This is used to locate the packer template.
.PARAMETER SecondsToWaitForServicePrincipalSetup
The number of seconds to wait for the service principal to be setup. The default is 120 seconds.
.PARAMETER AzureClientId
The Azure client id to use to authenticate with Azure. If not specified, the current user's credentials will be used.
.PARAMETER AzureClientSecret
The Azure client secret to use to authenticate with Azure. If not specified, the current user's credentials will be used.
.PARAMETER AzureTenantId
The Azure tenant id to use to authenticate with Azure. If not specified, the current user's credentials will be used.
.PARAMETER UseOidc
If set, authenticate using GitHub Actions OIDC (federated credentials) instead of a client secret.
Requires AzureClientId and AzureTenantId, and OidcRequestToken/OidcRequestUrl parameters.
.PARAMETER OidcRequestToken
GitHub Actions OIDC request token.
.PARAMETER OidcRequestUrl
GitHub Actions OIDC request URL.
.PARAMETER RestrictToAgentIpAddress
If set, access to the VM used by packer to generate the image is restricted to the public IP address this script is run from.
This parameter cannot be used in combination with the virtual_network_name packer parameter.
.PARAMETER OnError
Specify how packer handles an error during image creation.
Options:
abort - abort immediately
ask - ask user for input
cleanup - attempt to cleanup and then abort
run-cleanup-provisioner - run the cleanup provisioner and then abort
The default is 'ask'.
.PARAMETER Tags
Tags to be applied to the Azure resources created.
.PARAMETER PluginVersion
Specify the version of the packer Azure plugin to use. The default is "2.2.1".
.EXAMPLE
GenerateResourcesAndImage -SubscriptionId {YourSubscriptionId} -ResourceGroupName "shsamytest1" -ImageGenerationRepositoryRoot "C:\runner-images" -ImageType Ubuntu2204 -AzureLocation "East US"
#>
param (
[Parameter(Mandatory = $True)]
[string] $SubscriptionId,
[Parameter(Mandatory = $True)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $True)]
[ImageType] $ImageType,
[Parameter(Mandatory = $False)]
[string] $ManagedImageName = "Runner-Image-$($ImageType)",
[Parameter(Mandatory = $True)]
[string] $AzureLocation,
[Parameter(Mandatory = $False)]
[string] $ImageGenerationRepositoryRoot = $pwd,
[Parameter(Mandatory = $False)]
[int] $SecondsToWaitForServicePrincipalSetup = 120,
[Parameter(Mandatory = $False)]
[string] $AzureClientId,
[Parameter(Mandatory = $False)]
[string] $AzureClientSecret,
[Parameter(Mandatory = $False)]
[string] $AzureTenantId,
[Parameter(Mandatory = $False)]
[switch] $UseOidc,
[Parameter(Mandatory = $False)]
[ValidateNotNullOrEmpty()]
[string] $OidcRequestToken,
[Parameter(Mandatory = $False)]
[ValidateNotNullOrEmpty()]
[string] $OidcRequestUrl,
[Parameter(Mandatory = $False)]
[string] $PluginVersion = "2.2.1",
[Parameter(Mandatory = $False)]
[switch] $RestrictToAgentIpAddress,
[Parameter(Mandatory = $False)]
[ValidateSet("abort", "ask", "cleanup", "run-cleanup-provisioner")]
[string] $OnError = "ask",
[Parameter(Mandatory = $False)]
[hashtable] $Tags = @{}
)
Show-LatestCommit -ErrorAction SilentlyContinue
# Validate packer is installed
$PackerBinary = Get-Command "packer"
if (-not ($PackerBinary)) {
throw "'packer' binary is not found on PATH."
}
# Get template path
$PackerTemplate = Get-PackerTemplate -RepositoryRoot $ImageGenerationRepositoryRoot -ImageType $ImageType
Write-Debug "Template path: $($PackerTemplate.Path)."
# Prepare list of allowed inbound IP addresses
if ($RestrictToAgentIpAddress) {
$AgentIp = (Invoke-RestMethod https://ipinfo.io/json).ip
if (-not $AgentIp) {
throw "Unable to determine agent IP address."
}
Write-Host "Access to packer generated VM will be restricted to agent IP Address: $AgentIp."
if ($PSVersionTable.PSVersion.Major -eq 5) {
Write-Verbose "PowerShell 5 detected. Replacing double quotes with escaped double quotes in allowed inbound IP addresses."
$AllowedInboundIpAddresses = '[\"{0}\"]' -f $AgentIp
}
elseif ($PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -le 2) {
Write-Verbose "PowerShell 7.0-7.2 detected. Replacing double quotes with escaped double quotes in allowed inbound IP addresses."
$AllowedInboundIpAddresses = '[\"{0}\"]' -f $AgentIp
}
else {
$AllowedInboundIpAddresses = '["{0}"]' -f $AgentIp
}
}
else {
$AllowedInboundIpAddresses = "[]"
}
Write-Debug "Allowed inbound IP addresses: $AllowedInboundIpAddresses."
# Prepare tags
$TagsList = $Tags.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }
Write-Debug "Tags list: $TagsList."
$TagsJson = $Tags | ConvertTo-Json -Compress
if ($PSVersionTable.PSVersion.Major -eq 5) {
Write-Verbose "PowerShell 5 detected. Replacing double quotes with escaped double quotes in tags JSON."
$TagsJson = $TagsJson -replace '"', '\"'
}
elseif ($PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -le 2) {
Write-Verbose "PowerShell 7.0-7.2 detected. Replacing double quotes with escaped double quotes in tags JSON."
$TagsJson = $TagsJson -replace '"', '\"'
}
Write-Debug "Tags JSON: $TagsJson."
$InstallPassword = $env:UserName + [System.GUID]::NewGuid().ToString().ToUpper()
Write-Host "Downloading packer plugins..."
& $PackerBinary plugins install github.com/hashicorp/azure $PluginVersion
if ($LastExitCode -ne 0) {
throw "Packer plugins download failed."
}
Write-Host "Validating packer template..."
$validateClientSecret = "fake"
if ($UseOidc) {
$validateClientSecret = ""
}
& $PackerBinary validate `
"-only=$($PackerTemplate.BuildName).*" `
"-var=client_id=fake" `
"-var=client_secret=$($validateClientSecret)" `
"-var=oidc_request_token=fake" `
"-var=oidc_request_url=fake" `
"-var=subscription_id=$($SubscriptionId)" `
"-var=tenant_id=fake" `
"-var=location=$($AzureLocation)" `
"-var=image_os=$($PackerTemplate.ImageOS)" `
"-var=managed_image_name=$($ManagedImageName)" `
"-var=managed_image_resource_group_name=$($ResourceGroupName)" `
"-var=install_password=$($InstallPassword)" `
"-var=allowed_inbound_ip_addresses=$($AllowedInboundIpAddresses)" `
"-var=azure_tags=$($TagsJson)" `
$PackerTemplate.Path
if ($LastExitCode -ne 0) {
throw "Packer template validation failed."
}
try {
# Login to Azure subscription
if ([string]::IsNullOrEmpty($AzureClientId)) {
Write-Verbose "No AzureClientId was provided, will use interactive login."
az login --output none
}
elseif ($UseOidc) {
if ([string]::IsNullOrEmpty($AzureTenantId)) {
throw "AzureTenantId is required for OIDC authentication."
}
Write-Verbose "Using OIDC service principal login (federated credentials)."
$idToken = Get-GitHubActionsOidcIdToken -RequestUrl $OidcRequestUrl -RequestToken $OidcRequestToken
az login --service-principal --username $AzureClientId --tenant $AzureTenantId --federated-token $idToken --output none
}
else {
if ([string]::IsNullOrEmpty($AzureClientSecret) -or [string]::IsNullOrEmpty($AzureTenantId)) {
throw "AzureClientSecret and AzureTenantId are required for service principal login unless -UseOidc is specified."
}
Write-Verbose "AzureClientId was provided, will use service principal login (client secret)."
az login --service-principal --username $AzureClientId --password=$AzureClientSecret --tenant $AzureTenantId --output none
}
az account set --subscription $SubscriptionId
if ($LastExitCode -ne 0) {
throw "Failed to login to Azure subscription '$SubscriptionId'."
}
# Check resource group
$ResourceGroupExists = [System.Convert]::ToBoolean((az group exists --name $ResourceGroupName));
if ($ResourceGroupExists) {
Write-Verbose "Resource group '$ResourceGroupName' already exists."
}
else {
throw "Resource group '$ResourceGroupName' does not exist."
}
# Create / choose authentication for packer
if ([string]::IsNullOrEmpty($AzureClientId)) {
Write-Host "Creating service principal for packer..."
$ADCleanupRequired = $true
$ServicePrincipalName = "packer-" + [System.GUID]::NewGuid().ToString().ToUpper()
$ServicePrincipal = az ad sp create-for-rbac --name $ServicePrincipalName --role Contributor --scopes /subscriptions/$SubscriptionId --only-show-errors | ConvertFrom-Json
if ($LastExitCode -ne 0) {
throw "Failed to create service principal '$ServicePrincipalName'."
}
$ServicePrincipalAppId = $ServicePrincipal.appId
$ServicePrincipalPassword = $ServicePrincipal.password
$TenantId = $ServicePrincipal.tenant
Write-Verbose "Waiting for service principal to propagate..."
Start-Sleep $SecondsToWaitForServicePrincipalSetup
Write-Host "Service principal created with id '$ServicePrincipalAppId'. It will be deleted after the build."
}
else {
if ($UseOidc) {
if ([string]::IsNullOrEmpty($AzureTenantId)) {
throw "AzureTenantId is required for OIDC authentication."
}
$ServicePrincipalAppId = $AzureClientId
$ServicePrincipalPassword = ""
$TenantId = $AzureTenantId
# Avoid leaking OIDC request values via command line arguments.
$env:PKR_VAR_oidc_request_token = $OidcRequestToken
$env:PKR_VAR_oidc_request_url = $OidcRequestUrl
}
else {
if ([string]::IsNullOrEmpty($AzureClientSecret) -or [string]::IsNullOrEmpty($AzureTenantId)) {
throw "AzureClientSecret and AzureTenantId are required for service principal authentication unless -UseOidc is specified."
}
$ServicePrincipalAppId = $AzureClientId
$ServicePrincipalPassword = $AzureClientSecret
$TenantId = $AzureTenantId
}
}
Write-Debug "Service principal app id: $ServicePrincipalAppId."
Write-Debug "Tenant id: $TenantId."
& $PackerBinary build -on-error="$($OnError)" `
-only "$($PackerTemplate.BuildName).*" `
-var "client_id=$($ServicePrincipalAppId)" `
-var "client_secret=$($ServicePrincipalPassword)" `
-var "oidc_request_token=$($env:PKR_VAR_oidc_request_token)" `
-var "oidc_request_url=$($env:PKR_VAR_oidc_request_url)" `
-var "subscription_id=$($SubscriptionId)" `
-var "tenant_id=$($TenantId)" `
-var "location=$($AzureLocation)" `
-var "image_os=$($PackerTemplate.ImageOS)" `
-var "managed_image_name=$($ManagedImageName)" `
-var "managed_image_resource_group_name=$($ResourceGroupName)" `
-var "install_password=$($InstallPassword)" `
-var "allowed_inbound_ip_addresses=$($AllowedInboundIpAddresses)" `
-var "azure_tags=$($TagsJson)" `
$PackerTemplate.Path
if ($LastExitCode -ne 0) {
throw "Failed to build image."
}
} catch {
Write-Error $_
} finally {
Write-Verbose "`nCleaning up..."
# Remove ADServicePrincipal and ADApplication
if ($ADCleanupRequired) {
Write-Host "Removing ADServicePrincipal..."
if (az ad sp show --id $ServicePrincipalAppId --query id) {
az ad sp delete --id $ServicePrincipalAppId
}
Write-Host "Removing ADApplication..."
if (az ad app show --id $ServicePrincipalAppId --query id) {
az ad app delete --id $ServicePrincipalAppId
}
}
Write-Verbose "Cleanup completed."
}
}
================================================
FILE: helpers/GitHubApi.psm1
================================================
class GithubApi
{
[string] $Repository
[object] hidden $AuthHeader
GithubApi(
[string] $Repository,
[string] $AccessToken
) {
$this.Repository = $Repository
$this.AuthHeader = $this.BuildAuth($AccessToken)
}
[object] hidden BuildAuth([string]$AccessToken) {
if ([string]::IsNullOrEmpty($AccessToken)) {
return $null
}
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("'':${AccessToken}"))
return @{
Authorization = "Basic ${base64AuthInfo}"
}
}
[string] hidden BuildBaseUrl([string]$Repository, [string]$ApiPrefix) {
return "https://$ApiPrefix.github.com/repos/$Repository"
}
[object] GetWorkflowRuns([string]$WorkflowId) {
$url = "actions/workflows/$WorkflowId/runs"
$response = $this.InvokeRestMethod($url, 'GET', $null, $null)
return $response
}
[object] GetWorkflowRun([string]$WorkflowRunId) {
$url = "actions/runs/$WorkflowRunId"
$response = $this.InvokeRestMethod($url, 'GET', $null, $null)
return $response
}
[object] DispatchWorkflow([string]$EventType, [object]$EventPayload) {
$url = "dispatches"
$body = @{
"event_type" = $EventType
"client_payload" = $EventPayload
} | ConvertTo-Json
$response = $this.InvokeRestMethod($url, 'POST', $null, $body)
return $response
}
[object] CancelWorkflowRun([string]$workflowRunId) {
$url = "actions/runs/$workflowRunId/cancel"
$response = $this.InvokeRestMethod($url, 'POST', $null, $null)
return $response
}
[string] hidden BuildUrl([string]$url, [string]$RequestParams, [string]$ApiPrefix) {
$baseUrl = $this.BuildBaseUrl($this.Repository, $ApiPrefix)
if ([string]::IsNullOrEmpty($RequestParams)) {
return "$($baseUrl)/$($url)"
} else {
return "$($baseUrl)/$($url)?$($requestParams)"
}
}
[object] hidden InvokeRestMethod(
[string] $url,
[string] $Method,
[string] $RequestParams,
[string] $body
) {
$requestUrl = $this.BuildUrl($url, $RequestParams, "api")
$params = @{
Method = $Method
ContentType = "application/json"
Uri = $requestUrl
Headers = @{}
}
if ($this.AuthHeader) {
$params.Headers += $this.AuthHeader
}
if (![string]::IsNullOrEmpty($body)) {
$params.Body = $body
}
$response = Invoke-RestMethod @params
return $response
}
}
function Get-GithubApi {
param (
[string] $Repository,
[string] $AccessToken
)
return [GithubApi]::New($Repository, $AccessToken)
}
================================================
FILE: helpers/WaitWorkflowCompletion.ps1
================================================
Param (
[Parameter(Mandatory)]
[string] $WorkflowRunId,
[Parameter(Mandatory)]
[string] $Repository,
[Parameter(Mandatory)]
[string] $AccessToken,
[int] $RetryIntervalSeconds = 300,
[int] $MaxRetryCount = 0
)
Import-Module (Join-Path $PSScriptRoot "GitHubApi.psm1")
function Wait-ForWorkflowCompletion($WorkflowRunId, $RetryIntervalSeconds) {
do {
Start-Sleep -Seconds $RetryIntervalSeconds
$workflowRun = $gitHubApi.GetWorkflowRun($WorkflowRunId)
} until ($workflowRun.status -eq "completed")
return $workflowRun
}
$gitHubApi = Get-GithubApi -Repository $Repository -AccessToken $AccessToken
$attempt = 1
do {
$finishedWorkflowRun = Wait-ForWorkflowCompletion -WorkflowRunId $WorkflowRunId -RetryIntervalSeconds $RetryIntervalSeconds
Write-Host "Workflow run finished with result: $($finishedWorkflowRun.conclusion)"
if ($finishedWorkflowRun.conclusion -in ("success", "cancelled", "timed_out")) {
break
} elseif ($finishedWorkflowRun.conclusion -eq "failure") {
if ($attempt -le $MaxRetryCount) {
Write-Host "Workflow run will be restarted. Attempt $attempt of $MaxRetryCount"
$gitHubApi.ReRunFailedJobs($WorkflowRunId)
$attempt += 1
} else {
break
}
}
} while ($true)
Write-Host "Last result: $($finishedWorkflowRun.conclusion)."
"CI_WORKFLOW_RUN_RESULT=$($finishedWorkflowRun.conclusion)" | Out-File -Append -FilePath $env:GITHUB_ENV
if ($finishedWorkflowRun.conclusion -in ("failure", "cancelled", "timed_out")) {
exit 1
}
================================================
FILE: helpers/software-report-base/Calculate-ImagesDifference.ps1
================================================
using module ./SoftwareReport.psm1
using module ./SoftwareReport.DifferenceCalculator.psm1
<#
.SYNOPSIS
Calculates the difference between two software reports and saves it to a file.
.PARAMETER PreviousJsonReportPath
Path to the previous software report.
.PARAMETER CurrentJsonReportPath
Path to the current software report.
.PARAMETER OutputFile
Path to the file where the difference will be saved.
.PARAMETER ReleaseBranchName
Name of the release branch to build image docs URL.
.PARAMETER ReadmePath
Path to the README file in repository to build image docs URL.
#>
Param (
[Parameter(Mandatory=$true)]
[string] $PreviousJsonReportPath,
[Parameter(Mandatory=$true)]
[string] $CurrentJsonReportPath,
[Parameter(Mandatory=$true)]
[string] $OutputFile,
[Parameter(Mandatory=$false)]
[string] $ReleaseBranchName,
[Parameter(Mandatory=$false)]
[string] $ReadmePath
)
$ErrorActionPreference = "Stop"
$global:ErrorView = "NormalView"
function Read-SoftwareReport {
Param (
[Parameter(Mandatory=$true)]
[string] $JsonReportPath
)
if (-not (Test-Path $JsonReportPath)) {
throw "File '$JsonReportPath' does not exist"
}
$jsonReport = Get-Content -Path $JsonReportPath -Raw
$report = [SoftwareReport]::FromJson($jsonReport)
return $report
}
$previousReport = Read-SoftwareReport -JsonReportPath $PreviousJsonReportPath
$currentReport = Read-SoftwareReport -JsonReportPath $CurrentJsonReportPath
$comparer = [SoftwareReportDifferenceCalculator]::new($previousReport, $currentReport)
$comparer.CompareReports()
$diff = $comparer.GetMarkdownReport()
if ($ReleaseBranchName -and $ReadmePath) {
# https://github.com/actions/runner-images/blob/releases/macOS-12/20221215/images/macos/macos-12-Readme.md
$ImageDocsUrl = "https://github.com/actions/runner-images/blob/${ReleaseBranchName}/${ReadmePath}"
$diff += "`n`n`nFor comprehensive list of software installed on this image please click [here]($ImageDocsUrl)."
}
$parentDirectory = Split-Path $OutputFile -Parent
if (-not (Test-Path $parentDirectory)) { New-Item -Path $parentDirectory -ItemType Directory | Out-Null }
$diff | Out-File -Path $OutputFile -Encoding utf8NoBOM
================================================
FILE: helpers/software-report-base/SoftwareReport.BaseNodes.psm1
================================================
############################
### Abstract base nodes ####
############################
# Abstract base class for all nodes
class BaseNode {
[Boolean] ShouldBeIncludedToDiff() {
return $false
}
[String] ToMarkdown() {
return $this.ToMarkdown(1)
}
[String] ToMarkdown([Int32] $Level) {
throw "Abstract method 'ToMarkdown(level)' is not implemented for '$($this.GetType().Name)'"
}
[Boolean] IsSimilarTo([BaseNode] $OtherNode) {
throw "Abstract method 'IsSimilarTo' is not implemented for '$($this.GetType().Name)'"
}
[Boolean] IsIdenticalTo([BaseNode] $OtherNode) {
throw "Abstract method 'IsIdenticalTo' is not implemented for '$($this.GetType().Name)'"
}
}
# Abstract base class for all nodes that describe a tool and should be rendered inside diff table
class BaseToolNode: BaseNode {
[ValidateNotNullOrEmpty()]
[String] $ToolName
BaseToolNode([String] $ToolName) {
$this.ToolName = $ToolName
}
[Boolean] ShouldBeIncludedToDiff() {
return $true
}
[String] GetValue() {
throw "Abstract method 'GetValue' is not implemented for '$($this.GetType().Name)'"
}
[Boolean] IsSimilarTo([BaseNode] $OtherNode) {
if ($this.GetType() -ne $OtherNode.GetType()) {
return $false
}
return $this.ToolName -eq $OtherNode.ToolName
}
[Boolean] IsIdenticalTo([BaseNode] $OtherNode) {
return $this.IsSimilarTo($OtherNode) -and ($this.GetValue() -eq $OtherNode.GetValue())
}
}
================================================
FILE: helpers/software-report-base/SoftwareReport.DifferenceCalculator.psm1
================================================
using module ./SoftwareReport.psm1
using module ./SoftwareReport.BaseNodes.psm1
using module ./SoftwareReport.Nodes.psm1
using module ./SoftwareReport.DifferenceRender.psm1
class SoftwareReportDifferenceCalculator {
[ValidateNotNullOrEmpty()]
hidden [SoftwareReport] $PreviousReport
[ValidateNotNullOrEmpty()]
hidden [SoftwareReport] $CurrentReport
hidden [Collections.Generic.List[ReportDifferenceItem]] $AddedItems
hidden [Collections.Generic.List[ReportDifferenceItem]] $ChangedItems
hidden [Collections.Generic.List[ReportDifferenceItem]] $DeletedItems
SoftwareReportDifferenceCalculator([SoftwareReport] $PreviousReport, [SoftwareReport] $CurrentReport) {
$this.PreviousReport = $PreviousReport
$this.CurrentReport = $CurrentReport
}
[void] CompareReports() {
$this.AddedItems = @()
$this.ChangedItems = @()
$this.DeletedItems = @()
$this.CompareInternal($this.PreviousReport.Root, $this.CurrentReport.Root, @())
}
[String] GetMarkdownReport() {
$reporter = [SoftwareReportDifferenceRender]::new()
$report = $reporter.GenerateMarkdownReport($this.CurrentReport, $this.PreviousReport, $this.AddedItems, $this.ChangedItems, $this.DeletedItems)
return $report
}
hidden [void] CompareInternal([HeaderNode] $previousReportPointer, [HeaderNode] $currentReportPointer, [String[]] $Headers) {
$currentReportPointer.Children ?? @() | Where-Object { $_.ShouldBeIncludedToDiff() -and $this.FilterExcludedNodes($_) } | ForEach-Object {
$currentReportNode = $_
$sameNodeInPreviousReport = $previousReportPointer ? $previousReportPointer.FindSimilarChildNode($currentReportNode) : $null
if ($currentReportNode -is [HeaderNode]) {
# Compare HeaderNode recursively
$this.CompareInternal($sameNodeInPreviousReport, $currentReportNode, $Headers + $currentReportNode.Title)
} else {
if ($sameNodeInPreviousReport -and ($currentReportNode.IsIdenticalTo($sameNodeInPreviousReport))) {
# Nodes are identical, nothing changed, just ignore it
} elseif ($sameNodeInPreviousReport) {
# Nodes are equal but not identical, something was changed
if ($currentReportNode -is [TableNode]) {
$this.CompareSimilarTableNodes($sameNodeInPreviousReport, $currentReportNode, $Headers)
} elseif ($currentReportNode -is [ToolVersionsListNode]) {
$this.CompareSimilarToolVersionsListNodes($sameNodeInPreviousReport, $currentReportNode, $Headers)
} else {
$this.ChangedItems.Add([ReportDifferenceItem]::new($sameNodeInPreviousReport, $currentReportNode, $Headers))
}
} else {
# Node was not found in previous report, new node was added
$this.AddedItems.Add([ReportDifferenceItem]::new($null, $currentReportNode, $Headers))
}
}
}
# Detecting nodes that were removed
$previousReportPointer.Children ?? @() | Where-Object { $_.ShouldBeIncludedToDiff() -and $this.FilterExcludedNodes($_) } | ForEach-Object {
$previousReportNode = $_
$sameNodeInCurrentReport = $currentReportPointer ? $currentReportPointer.FindSimilarChildNode($previousReportNode) : $null
if (-not $sameNodeInCurrentReport) {
if ($previousReportNode -is [HeaderNode]) {
# Compare removed HeaderNode recursively
$this.CompareInternal($previousReportNode, $null, $Headers + $previousReportNode.Title)
} else {
# Node was not found in current report, node was removed
$this.DeletedItems.Add([ReportDifferenceItem]::new($previousReportNode, $null, $Headers))
}
}
}
}
hidden [void] CompareSimilarTableNodes([TableNode] $PreviousReportNode, [TableNode] $CurrentReportNode, [String[]] $Headers) {
$addedRows = $CurrentReportNode.Rows | Where-Object { $_ -notin $PreviousReportNode.Rows }
$deletedRows = $PreviousReportNode.Rows | Where-Object { $_ -notin $CurrentReportNode.Rows }
if (($addedRows.Count -eq 0) -and ($deletedRows.Count -eq 0)) {
# Unexpected state: TableNodes are identical
return
}
if ($PreviousReportNode.Headers -ne $CurrentReportNode.Headers) {
# If headers are changed and rows are changed at the same time, we should track it as removing table and adding new one
$this.DeletedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $null, $Headers))
$this.AddedItems.Add([ReportDifferenceItem]::new($null, $CurrentReportNode, $Headers))
} elseif (($addedRows.Count -gt 0) -and ($deletedRows.Count -eq 0)) {
# If new rows were added and no rows were deleted, then it is AddedItem
$this.AddedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers))
} elseif (($deletedRows.Count -gt 0) -and ($addedRows.Count -eq 0)) {
# If no rows were added and some rows were deleted, then it is DeletedItem
$this.DeletedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers))
} else {
# If some rows were added and some rows were removed, then it is UpdatedItem
$this.ChangedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers))
}
}
hidden [void] CompareSimilarToolVersionsListNodes([ToolVersionsListNode] $PreviousReportNode, [ToolVersionsListNode] $CurrentReportNode, [String[]] $Headers) {
$previousReportMajorVersions = $PreviousReportNode.Versions | ForEach-Object { $PreviousReportNode.ExtractMajorVersion($_) }
$currentReportMajorVersion = $CurrentReportNode.Versions | ForEach-Object { $CurrentReportNode.ExtractMajorVersion($_) }
$addedVersions = $CurrentReportNode.Versions | Where-Object { $CurrentReportNode.ExtractMajorVersion($_) -notin $previousReportMajorVersions }
$deletedVersions = $PreviousReportNode.Versions | Where-Object { $PreviousReportNode.ExtractMajorVersion($_) -notin $currentReportMajorVersion }
$changedPreviousVersions = $PreviousReportNode.Versions | Where-Object { ($PreviousReportNode.ExtractMajorVersion($_) -in $currentReportMajorVersion) -and ($_ -notin $CurrentReportNode.Versions) }
$changedCurrentVersions = $CurrentReportNode.Versions | Where-Object { ($CurrentReportNode.ExtractMajorVersion($_) -in $previousReportMajorVersions) -and ($_ -notin $PreviousReportNode.Versions) }
if ($addedVersions.Count -gt 0) {
$this.AddedItems.Add([ReportDifferenceItem]::new($null, [ToolVersionsListNode]::new($CurrentReportNode.ToolName, $addedVersions, $CurrentReportNode.MajorVersionRegex, "List"), $Headers))
}
if ($deletedVersions.Count -gt 0) {
$this.DeletedItems.Add([ReportDifferenceItem]::new([ToolVersionsListNode]::new($PreviousReportNode.ToolName, $deletedVersions, $PreviousReportNode.MajorVersionRegex, "List"), $null, $Headers))
}
$previousChangedNode = ($changedPreviousVersions.Count -gt 0) ? [ToolVersionsListNode]::new($PreviousReportNode.ToolName, $changedPreviousVersions, $PreviousReportNode.MajorVersionRegex, "List") : $null
$currentChangedNode = ($changedCurrentVersions.Count -gt 0) ? [ToolVersionsListNode]::new($CurrentReportNode.ToolName, $changedCurrentVersions, $CurrentReportNode.MajorVersionRegex, "List") : $null
if ($previousChangedNode -and $currentChangedNode) {
$this.ChangedItems.Add([ReportDifferenceItem]::new($previousChangedNode, $currentChangedNode, $Headers))
}
}
hidden [Boolean] FilterExcludedNodes([BaseNode] $Node) {
# We shouldn't show "Image Version" diff because it is already shown in report header
if (($Node -is [ToolVersionNode]) -and ($Node.ToolName -eq "Image Version:")) {
return $false
}
return $true
}
}
================================================
FILE: helpers/software-report-base/SoftwareReport.DifferenceRender.psm1
================================================
using module ./SoftwareReport.psm1
using module ./SoftwareReport.BaseNodes.psm1
using module ./SoftwareReport.Nodes.psm1
class SoftwareReportDifferenceRender {
[String] GenerateMarkdownReport([SoftwareReport] $CurrentReport, [SoftwareReport] $PreviousReport, [ReportDifferenceItem[]] $AddedItems, [ReportDifferenceItem[]] $ChangedItems, [ReportDifferenceItem[]] $DeletedItems) {
$sb = [System.Text.StringBuilder]::new()
$rootNode = $CurrentReport.Root
$imageVersion = $CurrentReport.GetImageVersion()
$previousImageVersion = $PreviousReport.GetImageVersion()
#############################
### Render report header ####
#############################
$sb.AppendLine("# :desktop_computer: Actions Runner Image: $($rootNode.Title)")
# ToolVersionNodes on root level contains main image description so just copy-paste them to final report
$rootNode.Children | Where-Object { $_ -is [ToolVersionNode] } | ForEach-Object {
$sb.AppendLine($_.ToMarkdown())
}
$sb.AppendLine()
$sb.AppendLine("## :mega: What's changed?").AppendLine()
###########################
### Render added items ####
###########################
[ReportDifferenceItem[]] $addedItemsBaseTools = $AddedItems | Where-Object { $_.IsBaseToolNode() }
[ReportDifferenceItem[]] $addedItemsTables = $AddedItems | Where-Object { $_.IsTableNode() }
if ($addedItemsBaseTools.Count + $addedItemsTables.Count -gt 0) {
$sb.AppendLine("### Added :heavy_plus_sign:").AppendLine()
}
if ($addedItemsBaseTools.Count -gt 0) {
$tableItems = $addedItemsBaseTools | ForEach-Object {
[PSCustomObject]@{
"Category" = $this.RenderCategory($_.Headers, $true);
"Tool name" = $this.RenderToolName($_.CurrentReportNode.ToolName);
"Current ($imageVersion)" = $_.CurrentReportNode.GetValue();
}
}
$sb.AppendLine($this.RenderHtmlTable($tableItems, "Category"))
}
if ($addedItemsTables.Count -gt 0) {
$addedItemsTables | ForEach-Object {
$sb.AppendLine($this.RenderTableNodesDiff($_))
}
}
#############################
### Render deleted items ####
#############################
[ReportDifferenceItem[]] $deletedItemsBaseTools = $DeletedItems | Where-Object { $_.IsBaseToolNode() }
[ReportDifferenceItem[]] $deletedItemsTables = $DeletedItems | Where-Object { $_.IsTableNode() }
if ($deletedItemsBaseTools.Count + $deletedItemsTables.Count -gt 0) {
$sb.AppendLine("### Deleted :heavy_minus_sign:").AppendLine()
}
if ($deletedItemsBaseTools.Count -gt 0) {
$tableItems = $deletedItemsBaseTools | ForEach-Object {
[PSCustomObject]@{
"Category" = $this.RenderCategory($_.Headers, $true);
"Tool name" = $this.RenderToolName($_.PreviousReportNode.ToolName);
"Previous ($previousImageVersion)" = $_.PreviousReportNode.GetValue();
}
}
$sb.AppendLine($this.RenderHtmlTable($tableItems, "Category"))
}
if ($deletedItemsTables.Count -gt 0) {
$deletedItemsTables | ForEach-Object {
$sb.AppendLine($this.RenderTableNodesDiff($_))
}
}
#############################
### Render updated items ####
#############################
[ReportDifferenceItem[]] $changedItemsBaseTools = $ChangedItems | Where-Object { $_.IsBaseToolNode() }
[ReportDifferenceItem[]] $changedItemsTables = $ChangedItems | Where-Object { $_.IsTableNode() }
if ($changedItemsBaseTools.Count + $changedItemsTables.Count -gt 0) {
$sb.AppendLine("### Updated").AppendLine()
}
if ($changedItemsBaseTools.Count -gt 0) {
$tableItems = $changedItemsBaseTools | ForEach-Object {
[PSCustomObject]@{
"Category" = $this.RenderCategory($_.Headers, $true);
"Tool name" = $this.RenderToolName($_.CurrentReportNode.ToolName);
"Previous ($previousImageVersion)" = $_.PreviousReportNode.GetValue();
"Current ($imageVersion)" = $_.CurrentReportNode.GetValue();
}
}
$sb.AppendLine($this.RenderHtmlTable($tableItems, "Category"))
}
if ($changedItemsTables.Count -gt 0) {
$changedItemsTables | ForEach-Object {
$sb.AppendLine($this.RenderTableNodesDiff($_))
}
}
return $sb.ToString()
}
[String] RenderHtmlTable([PSCustomObject[]] $Table, [String] $RowSpanColumnName) {
$headers = $Table[0].PSObject.Properties.Name
$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine("<table>")
$sb.AppendLine(" <thead>")
$headers | ForEach-Object {
$sb.AppendLine(" <th>$_</th>")
}
$sb.AppendLine(" </thead>")
$sb.AppendLine(" <tbody>")
$tableRowSpans = $this.CalculateHtmlTableRowSpan($Table, $RowSpanColumnName)
for ($rowIndex = 0; $rowIndex -lt $Table.Count; $rowIndex++) {
$row = $Table[$rowIndex]
$sb.AppendLine(" <tr>")
$headers | ForEach-Object {
if ($_ -eq $RowSpanColumnName) {
if ($tableRowSpans[$rowIndex] -gt 0) {
$sb.AppendLine(" <td rowspan=`"$($tableRowSpans[$rowIndex])`">$($row.$_)</td>")
} else {
# Skip rendering this cell at all
}
} else {
$sb.AppendLine(" <td>$($row.$_)</td>")
}
}
$sb.AppendLine(" </tr>")
}
$sb.AppendLine(" </tbody>")
$sb.AppendLine("</table>")
return $sb.ToString()
}
[int[]] CalculateHtmlTableRowSpan([PSCustomObject[]] $Table, [String] $keyColumn) {
$result = @(0) * $Table.Count
for ($rowIndex = $Table.Count - 1; $rowIndex -ge 0; $rowIndex--) {
if (($rowIndex -lt ($Table.Count - 1)) -and ($Table[$rowIndex].$keyColumn -eq $Table[$rowIndex + 1].$keyColumn)) {
# If the current row is the same as the next row
# Then rowspan of current row should be equal to rowspan of the next row + 1
# And rowspan of the next row should be 0 because it is already included in the rowspan of the current row
$result[$rowIndex] = $result[$rowIndex + 1] + 1
$result[$rowIndex + 1] = 0
} else {
$result[$rowIndex] = 1
}
}
return $result
}
[String] RenderTableNodesDiff([ReportDifferenceItem] $DiffItem) {
# Use the simplest approach for now: first, print all removed lines. Then print added lines
# It will work well for most cases like changing existing rows, adding new rows and removing rows
# But can produce not so pretty results for cases when some rows are changed and some rows are added at the same time
# Let's see how it works in practice and improve it later if needed
[String] $tableHeaders = ($DiffItem.CurrentReportNode ?? $DiffItem.PreviousReportNode).Headers
[Collections.Generic.List[String]] $tableRows = @()
$DiffItem.PreviousReportNode.Rows ?? @() | Where-Object { $_ -notin $DiffItem.CurrentReportNode.Rows } | ForEach-Object {
$tableRows.Add($this.StrikeTableRow($_))
}
$DiffItem.CurrentReportNode.Rows ?? @() | Where-Object { $_ -notin $DiffItem.PreviousReportNode.Rows } | ForEach-Object {
$tableRows.Add($_)
}
$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine("#### $($this.RenderCategory($DiffItem.Headers, $false))")
$sb.AppendLine([TableNode]::new($tableHeaders, $tableRows).ToMarkdown())
return $sb.ToString()
}
[String] RenderCategory([String[]] $Headers, [Boolean] $AddLineSeparator) {
# Always skip the first header because it is "Installed Software"
[String[]] $takeHeaders = $Headers | Select-Object -Skip 1
if ($takeHeaders.Count -eq 0) {
return ""
}
$lineSeparator = $AddLineSeparator ? "<br>": ""
return [String]::Join(" >$lineSeparator ", $takeHeaders)
}
[String] RenderToolName([String] $ToolName) {
return $ToolName.TrimEnd(":")
}
[String] StrikeTableRow([String] $Row) {
# Convert "a|b|c" to "~~a~~|~~b~~|~~c~~
$cells = $Row.Split("|")
$strikedCells = $cells | ForEach-Object { "~~$($_)~~"}
return [String]::Join("|", $strikedCells)
}
}
# Temporary structure to store the single difference between two reports
class ReportDifferenceItem {
[BaseNode] $PreviousReportNode
[BaseNode] $CurrentReportNode
[String[]] $Headers
ReportDifferenceItem([BaseNode] $PreviousReportNode, [BaseNode] $CurrentReportNode, [String[]] $Headers) {
$this.PreviousReportNode = $PreviousReportNode
$this.CurrentReportNode = $CurrentReportNode
$this.Headers = $Headers
}
[Boolean] IsBaseToolNode() {
$node = $this.CurrentReportNode ?? $this.PreviousReportNode
return $node -is [BaseToolNode]
}
[Boolean] IsTableNode() {
$node = $this.CurrentReportNode ?? $this.PreviousReportNode
return $node -is [TableNode]
}
}
================================================
FILE: helpers/software-report-base/SoftwareReport.Nodes.psm1
================================================
using module ./SoftwareReport.BaseNodes.psm1
#########################################
### Nodes to describe image software ####
#########################################
# NodesFactory is used to simplify parsing different types of notes
# Every node has own logic of parsing and this method just invokes "FromJsonObject" of correct node type
class NodesFactory {
static [BaseNode] ParseNodeFromObject([object] $JsonObj) {
if ($JsonObj.NodeType -eq [HeaderNode].Name) {
return [HeaderNode]::FromJsonObject($JsonObj)
} elseif ($JsonObj.NodeType -eq [ToolVersionNode].Name) {
return [ToolVersionNode]::FromJsonObject($JsonObj)
} elseif ($JsonObj.NodeType -eq [ToolVersionsListNode].Name) {
return [ToolVersionsListNode]::FromJsonObject($JsonObj)
} elseif ($JsonObj.NodeType -eq [TableNode].Name) {
return [TableNode]::FromJsonObject($JsonObj)
} elseif ($JsonObj.NodeType -eq [NoteNode].Name) {
return [NoteNode]::FromJsonObject($JsonObj)
}
throw "Unknown node type in ParseNodeFromObject '$($JsonObj.NodeType)'"
}
}
class HeaderNode: BaseNode {
[ValidateNotNullOrEmpty()]
[String] $Title
[Collections.Generic.List[BaseNode]] $Children
HeaderNode([String] $Title) {
$this.Title = $Title
$this.Children = @()
}
[Boolean] ShouldBeIncludedToDiff() {
return $true
}
[void] AddNode([BaseNode] $node) {
$similarNode = $this.FindSimilarChildNode($node)
if ($similarNode) {
throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.`nFound node: $($similarNode.ToJsonObject() | ConvertTo-Json)`nNew node: $($node.ToJsonObject() | ConvertTo-Json)"
}
if (-not $this.IsNodeHasMarkdownHeader($node)) {
# If the node doesn't print own header to markdown, we should check that there is no other nodes that print header to markdown before it.
# It is done to avoid unexpected situation like this:
#
# HeaderNode A -> # A
# HeaderNode B -> ## B
# ToolVersionNode C -> - C
# ToolVersionNode D -> - D
#
# In this example, we add 'HeaderNode B" to 'HeaderNode A' and add 'ToolVersionNode C' to 'HeaderNode B'.
# Then we add 'ToolVersionNode D' to 'HeaderNode A'.
# But the result markdown will look like 'ToolVersionNode D' belongs to 'HeaderNode B' instead of 'HeaderNode A'.
$this.Children | Where-Object { $this.IsNodeHasMarkdownHeader($_) } | ForEach-Object {
throw "It is not allowed to add the non-header node after the header node. Consider adding the separate HeaderNode for this node"
}
}
$this.Children.Add($node)
}
[void] AddNodes([BaseNode[]] $nodes) {
$nodes | ForEach-Object {
$this.AddNode($_)
}
}
[HeaderNode] AddHeader([String] $Title) {
$node = [HeaderNode]::new($Title)
$this.AddNode($node)
return $node
}
[void] AddToolVersion([String] $ToolName, [String] $Version) {
$this.AddNode([ToolVersionNode]::new($ToolName, $Version))
}
[void] AddToolVersionsList([String] $ToolName, [String[]] $Version, [String] $MajorVersionRegex) {
$this.AddNode([ToolVersionsListNode]::new($ToolName, $Version, $MajorVersionRegex, "List"))
}
[void] AddToolVersionsListInline([String] $ToolName, [String[]] $Version, [String] $MajorVersionRegex) {
$this.AddNode([ToolVersionsListNode]::new($ToolName, $Version, $MajorVersionRegex, "Inline"))
}
[void] AddTable([PSCustomObject[]] $Table) {
$this.AddNode([TableNode]::FromObjectsArray($Table))
}
[void] AddNote([String] $Content) {
$this.AddNode([NoteNode]::new($Content))
}
[String] ToMarkdown([Int32] $Level) {
$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine()
$sb.AppendLine("$("#" * $Level) $($this.Title)")
$this.Children | ForEach-Object {
$sb.AppendLine($_.ToMarkdown($Level + 1))
}
return $sb.ToString().TrimEnd()
}
[PSCustomObject] ToJsonObject() {
return [PSCustomObject]@{
NodeType = $this.GetType().Name
Title = $this.Title
Children = $this.Children | ForEach-Object { $_.ToJsonObject() }
}
}
static [HeaderNode] FromJsonObject([Object] $JsonObj) {
$node = [HeaderNode]::new($JsonObj.Title)
$JsonObj.Children | Where-Object { $_ } | ForEach-Object { $node.AddNode([NodesFactory]::ParseNodeFromObject($_)) }
return $node
}
[Boolean] IsSimilarTo([BaseNode] $OtherNode) {
if ($OtherNode.GetType() -ne [HeaderNode]) {
return $false
}
return $this.Title -eq $OtherNode.Title
}
[Boolean] IsIdenticalTo([BaseNode] $OtherNode) {
return $this.IsSimilarTo($OtherNode)
}
[BaseNode] FindSimilarChildNode([BaseNode] $Find) {
foreach ($childNode in $this.Children) {
if ($childNode.IsSimilarTo($Find)) {
return $childNode
}
}
return $null
}
hidden [Boolean] IsNodeHasMarkdownHeader([BaseNode] $node) {
if ($node -is [HeaderNode]) {
return $true
}
if (($node -is [ToolVersionsListNode]) -and ($node.ListType -eq "List")) {
return $true
}
return $false
}
}
class ToolVersionNode: BaseToolNode {
[ValidateNotNullOrEmpty()]
[String] $Version
ToolVersionNode([String] $ToolName, [String] $Version): base($ToolName) {
if ([String]::IsNullOrEmpty($Version)) {
throw "ToolVersionNode '$($this.ToolName)' has empty version"
}
$this.Version = $Version
}
[String] ToMarkdown([Int32] $Level) {
return "- $($this.ToolName) $($this.Version)"
}
[String] GetValue() {
return $this.Version
}
[PSCustomObject] ToJsonObject() {
return [PSCustomObject]@{
NodeType = $this.GetType().Name
ToolName = $this.ToolName
Version = $this.Version
}
}
static [BaseNode] FromJsonObject([Object] $JsonObj) {
return [ToolVersionNode]::new($JsonObj.ToolName, $JsonObj.Version)
}
}
class ToolVersionsListNode: BaseToolNode {
[ValidateNotNullOrEmpty()]
[String[]] $Versions
[Regex] $MajorVersionRegex
[ValidateSet("List", "Inline")]
[String] $ListType
ToolVersionsListNode([String] $ToolName, [String[]] $Versions, [String] $MajorVersionRegex, [String] $ListType): base($ToolName) {
$this.Versions = $Versions
if ([String]::IsNullOrEmpty($Versions)) {
throw "ToolVersionsListNode '$($this.ToolName)' has empty versions list"
}
$this.MajorVersionRegex = [Regex]::new($MajorVersionRegex)
$this.ListType = $ListType
$this.ValidateMajorVersionRegex()
}
[String] ToMarkdown([Int32] $Level) {
if ($this.ListType -eq "Inline") {
return "- $($this.ToolName): $($this.Versions -join ', ')"
}
$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine()
$sb.AppendLine("$("#" * $Level) $($this.ToolName)")
$this.Versions | ForEach-Object {
$sb.AppendLine("- $_")
}
return $sb.ToString().TrimEnd()
}
[String] GetValue() {
return $this.Versions -join ', '
}
[String] ExtractMajorVersion([String] $Version) {
$match = $this.MajorVersionRegex.Match($Version)
if (($match.Success -ne $true) -or [String]::IsNullOrEmpty($match.Groups[0].Value)) {
throw "Version '$Version' doesn't match regex '$($this.PrimaryVersionRegex)'"
}
return $match.Groups[0].Value
}
[PSCustomObject] ToJsonObject() {
return [PSCustomObject]@{
NodeType = $this.GetType().Name
ToolName = $this.ToolName
Versions = $this.Versions
MajorVersionRegex = $this.MajorVersionRegex.ToString()
ListType = $this.ListType
}
}
static [ToolVersionsListNode] FromJsonObject([Object] $JsonObj) {
return [ToolVersionsListNode]::new($JsonObj.ToolName, $JsonObj.Versions, $JsonObj.MajorVersionRegex, $JsonObj.ListType)
}
hidden [void] ValidateMajorVersionRegex() {
$this.Versions | Group-Object { $this.ExtractMajorVersion($_) } | ForEach-Object {
if ($_.Count -gt 1) {
throw "Multiple versions from list '$($this.GetValue())' return the same result from regex '$($this.MajorVersionRegex)': $($_.Name)"
}
}
}
}
class TableNode: BaseNode {
# It is easier to store the table as rendered lines because it will simplify finding differences in rows later
[ValidateNotNullOrEmpty()]
[String] $Headers
[ValidateNotNullOrEmpty()]
[String[]] $Rows
TableNode([String] $Headers, [String[]] $Rows) {
$this.Headers = $Headers
$this.Rows = $Rows
$columnsCount = $this.Headers.Split("|").Count
$this.Rows | ForEach-Object {
if ($_.Split("|").Count -ne $columnsCount) {
throw "Table has different number of columns in different rows"
}
}
}
[Boolean] ShouldBeIncludedToDiff() {
return $true
}
[String] ToMarkdown([Int32] $Level) {
$maxColumnWidths = $this.CalculateColumnsWidth()
$columnsCount = $maxColumnWidths.Count
$delimiterLine = [String]::Join("|", @("-") * $columnsCount)
$sb = [System.Text.StringBuilder]::new()
@($this.Headers) + @($delimiterLine) + $this.Rows | ForEach-Object {
$sb.Append("|")
$row = $_.Split("|")
for ($colIndex = 0; $colIndex -lt $columnsCount; $colIndex++) {
$padSymbol = $row[$colIndex] -eq "-" ? "-" : " "
$cellContent = $row[$colIndex].PadRight($maxColumnWidths[$colIndex], $padSymbol)
$sb.Append(" $($cellContent) |")
}
$sb.AppendLine()
}
return $sb.ToString().TrimEnd()
}
hidden [Int32[]] CalculateColumnsWidth() {
$maxColumnWidths = $this.Headers.Split("|") | ForEach-Object { $_.Length }
$columnsCount = $maxColumnWidths.Count
$this.Rows | ForEach-Object {
$columnWidths = $_.Split("|") | ForEach-Object { $_.Length }
for ($colIndex = 0; $colIndex -lt $columnsCount; $colIndex++) {
$maxColumnWidths[$colIndex] = [Math]::Max($maxColumnWidths[$colIndex], $columnWidths[$colIndex])
}
}
return $maxColumnWidths
}
[PSCustomObject] ToJsonObject() {
return [PSCustomObject]@{
NodeType = $this.GetType().Name
Headers = $this.Headers
Rows = $this.Rows
}
}
static [TableNode] FromJsonObject([Object] $JsonObj) {
return [TableNode]::new($JsonObj.Headers, $JsonObj.Rows)
}
[Boolean] IsSimilarTo([BaseNode] $OtherNode) {
if ($OtherNode.GetType() -ne [TableNode]) {
return $false
}
# We don't support having multiple TableNode instances on the same header level so such check is fine
return $true
}
[Boolean] IsIdenticalTo([BaseNode] $OtherNode) {
if (-not $this.IsSimilarTo($OtherNode)) {
return $false
}
# We don't compare $this.Headers intentionally
# It is fine to ignore the tables where headers are changed but rows are not changed
if ($this.Rows.Count -ne $OtherNode.Rows.Count) {
return $false
}
for ($rowIndex = 0; $rowIndex -lt $this.Rows.Count; $rowIndex++) {
if ($this.Rows[$rowIndex] -ne $OtherNode.Rows[$rowIndex]) {
return $false
}
}
return $true
}
static [TableNode] FromObjectsArray([PSCustomObject[]] $Table) {
if ($Table.Count -eq 0) {
throw "Failed to create TableNode from empty objects array"
}
[String] $tableHeaders = [TableNode]::ArrayToTableRow($Table[0].PSObject.Properties.Name)
[Collections.Generic.List[String]] $tableRows = @()
$Table | ForEach-Object {
$rowHeaders = [TableNode]::ArrayToTableRow($_.PSObject.Properties.Name)
if (($rowHeaders -ne $tableHeaders)) {
throw "Failed to create TableNode from objects array because objects have different properties"
}
$tableRows.Add([TableNode]::ArrayToTableRow($_.PSObject.Properties.Value))
}
return [TableNode]::new($tableHeaders, $tableRows)
}
hidden static [String] ArrayToTableRow([String[]] $Values) {
if ($Values.Count -eq 0) {
throw "Failed to create TableNode because some objects are empty"
}
$Values | ForEach-Object {
if ($_.Contains("|")) {
throw "Failed to create TableNode because some cells '$_' contains forbidden symbol '|'"
}
}
return [String]::Join("|", $Values)
}
}
class NoteNode: BaseNode {
[ValidateNotNullOrEmpty()]
[String] $Content
NoteNode([String] $Content) {
$this.Content = $Content
}
[String] ToMarkdown([Int32] $Level) {
return @(
'```',
$this.Content,
'```'
) -join "`n"
}
[PSCustomObject] ToJsonObject() {
return [PSCustomObject]@{
NodeType = $this.GetType().Name
Content = $this.Content
}
}
static [NoteNode] FromJsonObject([Object] $JsonObj) {
return [NoteNode]::new($JsonObj.Content)
}
[Boolean] IsSimilarTo([BaseNode] $OtherNode) {
if ($OtherNode.GetType() -ne [NoteNode]) {
return $false
}
return $this.Content -eq $OtherNode.Content
}
[Boolean] IsIdenticalTo([BaseNode] $OtherNode) {
return $this.IsSimilarTo($OtherNode)
}
}
================================================
FILE: helpers/software-report-base/SoftwareReport.psm1
================================================
using module ./SoftwareReport.BaseNodes.psm1
using module ./SoftwareReport.Nodes.psm1
class SoftwareReport {
[ValidateNotNullOrEmpty()]
[HeaderNode] $Root
SoftwareReport([String] $Title) {
$this.Root = [HeaderNode]::new($Title)
}
SoftwareReport([HeaderNode] $Root) {
$this.Root = $Root
}
[String] ToJson() {
return $this.Root.ToJsonObject() | ConvertTo-Json -Depth 10
}
static [SoftwareReport] FromJson([String] $JsonString) {
$jsonObj = $JsonString | ConvertFrom-Json
$rootNode = [NodesFactory]::ParseNodeFromObject($jsonObj)
return [SoftwareReport]::new($rootNode)
}
[String] ToMarkdown() {
return $this.Root.ToMarkdown().Trim()
}
[String] GetImageVersion() {
$imageVersionNode = $this.Root.Children ?? @() | Where-Object { ($_ -is [ToolVersionNode]) -and ($_.ToolName -eq "Image Version:") } | Select-Object -First 1
return $imageVersionNode.Version ?? "Unknown version"
}
}
================================================
FILE: helpers/software-report-base/tests/SoftwareReport.Difference.E2E.Tests.ps1
================================================
using module ../SoftwareReport.psm1
using module ../SoftwareReport.DifferenceCalculator.psm1
Describe "Comparer.E2E" {
It "Some tools are updated" {
# Previous report
$prevSoftwareReport = [SoftwareReport]::new("macOS 11")
$prevSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.1 (20G817)")
$prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1")
$prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software")
$prevTools = $prevInstalledSoftware.AddHeader("Tools")
$prevTools.AddToolVersion("ToolWillBeUpdated1", "1.0.0")
$prevTools.AddToolVersion("ToolWillBeUpdated2", "3.0.1")
$prevTools.AddToolVersionsList("ToolWillBeUpdated3", @("14.0.0", "15.5.1"), "^\d+")
# Next report
$nextSoftwareReport = [SoftwareReport]::new("macOS 11")
$nextSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.1 (20G817)")
$nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.1")
$nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software")
$nextTools = $nextInstalledSoftware.AddHeader("Tools")
$nextTools.AddToolVersion("ToolWillBeUpdated1", "2.5.0")
$nextTools.AddToolVersion("ToolWillBeUpdated2", "3.0.2")
$nextTools.AddToolVersionsList("ToolWillBeUpdated3", @("14.2.0", "15.5.1"), "^\d+")
# Compare reports
$comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport)
$comparer.CompareReports()
$comparer.GetMarkdownReport() | Should -BeExactly @'
# :desktop_computer: Actions Runner Image: macOS 11
- OS Version: macOS 11.7.1 (20G817)
- Image Version: 20220922.1
## :mega: What's changed?
### Updated
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Previous (20220918.1)</th>
<th>Current (20220922.1)</th>
</thead>
<tbody>
<tr>
<td rowspan="3">Tools</td>
<td>ToolWillBeUpdated1</td>
<td>1.0.0</td>
<td>2.5.0</td>
</tr>
<tr>
<td>ToolWillBeUpdated2</td>
<td>3.0.1</td>
<td>3.0.2</td>
</tr>
<tr>
<td>ToolWillBeUpdated3</td>
<td>14.0.0</td>
<td>14.2.0</td>
</tr>
</tbody>
</table>
'@
}
It "Some tools are updated, added and removed" {
# Previous report
$prevSoftwareReport = [SoftwareReport]::new("macOS 11")
$prevSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.1 (20G817)")
$prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1")
$prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software")
$prevLanguagesAndRuntimes = $prevInstalledSoftware.AddHeader("Language and Runtime")
$prevLanguagesAndRuntimes.AddToolVersion("ToolWillBeRemoved", "5.1.16(1)-release")
$prevLanguagesAndRuntimes.AddToolVersionsListInline("ToolWithMultipleVersions3", @("1.2.100", "1.2.200", "1.3.500", "1.4.100", "1.4.200"), "^\d+\.\d+\.\d")
$prevLanguagesAndRuntimes.AddToolVersion("ToolWithoutChanges", "5.34.0")
$prevLanguagesAndRuntimes.AddToolVersion("ToolWillBeUpdated", "8.1.0")
$prevCachedTools = $prevInstalledSoftware.AddHeader("Cached Tools")
$prevCachedTools.AddToolVersionsList("ToolWithMultipleVersions1", @("2.7.3", "2.8.1", "3.1.2"), "^\d+\.\d+")
$prevCachedTools.AddToolVersionsList("ToolWithMultipleVersions2", @("14.8.0", "15.1.0", "16.4.2"), "^\d+")
$prevSQLSection = $prevInstalledSoftware.AddHeader("Databases")
$prevSQLSection.AddToolVersion("MineSQL", "6.1.0")
$prevSQLSection.AddNote("First Note")
# Next report
$nextSoftwareReport = [SoftwareReport]::new("macOS 11")
$nextSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.2 (20G922)")
$nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.0")
$nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software")
$nextLanguagesAndRuntimes = $nextInstalledSoftware.AddHeader("Language and Runtime")
$nextLanguagesAndRuntimes.AddToolVersion("ToolWillBeAdded", "16.18.0")
$nextLanguagesAndRuntimes.AddToolVersionsListInline("ToolWithMultipleVersions3", @("1.2.200", "1.3.515", "1.4.100", "1.4.200", "1.5.800"), "^\d+\.\d+\.\d")
$nextLanguagesAndRuntimes.AddToolVersion("ToolWithoutChanges", "5.34.0")
$nextLanguagesAndRuntimes.AddToolVersion("ToolWillBeUpdated", "8.3.0")
$nextCachedTools = $nextInstalledSoftware.AddHeader("Cached Tools")
$nextCachedTools.AddToolVersionsList("ToolWithMultipleVersions1", @("2.7.3", "2.8.1", "3.1.2"), "^\d+\.\d+")
$nextCachedTools.AddToolVersionsList("ToolWithMultipleVersions2", @("15.1.0", "16.4.2", "17.0.1"), "^\d+")
$nextSQLSection = $nextInstalledSoftware.AddHeader("Databases")
$nextSQLSection.AddToolVersion("MineSQL", "6.1.1")
$nextSQLSection.AddNote("Second Note")
# Compare reports
$comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport)
$comparer.CompareReports()
$comparer.GetMarkdownReport() | Should -BeExactly @'
# :desktop_computer: Actions Runner Image: macOS 11
- OS Version: macOS 11.7.2 (20G922)
- Image Version: 20220922.0
## :mega: What's changed?
### Added :heavy_plus_sign:
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Current (20220922.0)</th>
</thead>
<tbody>
<tr>
<td rowspan="2">Language and Runtime</td>
<td>ToolWillBeAdded</td>
<td>16.18.0</td>
</tr>
<tr>
<td>ToolWithMultipleVersions3</td>
<td>1.5.800</td>
</tr>
<tr>
<td rowspan="1">Cached Tools</td>
<td>ToolWithMultipleVersions2</td>
<td>17.0.1</td>
</tr>
</tbody>
</table>
### Deleted :heavy_minus_sign:
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Previous (20220918.1)</th>
</thead>
<tbody>
<tr>
<td rowspan="2">Language and Runtime</td>
<td>ToolWithMultipleVersions3</td>
<td>1.2.100</td>
</tr>
<tr>
<td>ToolWillBeRemoved</td>
<td>5.1.16(1)-release</td>
</tr>
<tr>
<td rowspan="1">Cached Tools</td>
<td>ToolWithMultipleVersions2</td>
<td>14.8.0</td>
</tr>
</tbody>
</table>
### Updated
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Previous (20220918.1)</th>
<th>Current (20220922.0)</th>
</thead>
<tbody>
<tr>
<td rowspan="1"></td>
<td>OS Version</td>
<td>macOS 11.7.1 (20G817)</td>
<td>macOS 11.7.2 (20G922)</td>
</tr>
<tr>
<td rowspan="2">Language and Runtime</td>
<td>ToolWithMultipleVersions3</td>
<td>1.3.500</td>
<td>1.3.515</td>
</tr>
<tr>
<td>ToolWillBeUpdated</td>
<td>8.1.0</td>
<td>8.3.0</td>
</tr>
<tr>
<td rowspan="1">Databases</td>
<td>MineSQL</td>
<td>6.1.0</td>
<td>6.1.1</td>
</tr>
</tbody>
</table>
'@
}
It "Header tree changes" {
# Previous report
$prevSoftwareReport = [SoftwareReport]::new("macOS 11")
$prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1")
$prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software")
$prevInstalledSoftware.AddToolVersion("ToolWithoutChanges", "5.34.0")
$prevInstalledSoftware.AddHeader("HeaderWillBeRemoved").AddHeader("SubheaderWillBeRemoved").AddToolVersion("ToolWillBeRemoved", "1.0.0")
$prevInstalledSoftware.AddHeader("Header1").AddToolVersion("ToolWillBeMovedToAnotherHeader", "3.0.0")
# Next report
$nextSoftwareReport = [SoftwareReport]::new("macOS 11")
$nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.0")
$nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software")
$nextInstalledSoftware.AddToolVersion("ToolWithoutChanges", "5.34.0")
$nextInstalledSoftware.AddHeader("HeaderWillBeAdded").AddHeader("SubheaderWillBeAdded").AddToolVersion("ToolWillBeAdded", "5.0.0")
$nextInstalledSoftware.AddHeader("Header2").AddToolVersion("ToolWillBeMovedToAnotherHeader", "3.0.0")
# Compare reports
$comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport)
$comparer.CompareReports()
$comparer.GetMarkdownReport() | Should -BeExactly @'
# :desktop_computer: Actions Runner Image: macOS 11
- Image Version: 20220922.0
## :mega: What's changed?
### Added :heavy_plus_sign:
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Current (20220922.0)</th>
</thead>
<tbody>
<tr>
<td rowspan="1">HeaderWillBeAdded ><br> SubheaderWillBeAdded</td>
<td>ToolWillBeAdded</td>
<td>5.0.0</td>
</tr>
<tr>
<td rowspan="1">Header2</td>
<td>ToolWillBeMovedToAnotherHeader</td>
<td>3.0.0</td>
</tr>
</tbody>
</table>
### Deleted :heavy_minus_sign:
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Previous (20220918.1)</th>
</thead>
<tbody>
<tr>
<td rowspan="1">HeaderWillBeRemoved ><br> SubheaderWillBeRemoved</td>
<td>ToolWillBeRemoved</td>
<td>1.0.0</td>
</tr>
<tr>
<td rowspan="1">Header1</td>
<td>ToolWillBeMovedToAnotherHeader</td>
<td>3.0.0</td>
</tr>
</tbody>
</table>
'@
}
It "Tables are added and removed" {
# Previous report
$prevSoftwareReport = [SoftwareReport]::new("macOS 11")
$prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1")
$prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software")
$prevInstalledSoftware.AddHeader("HeaderWillExist").AddTable(@(
[PSCustomObject]@{TableInExistingHeaderWillBeRemoved = "Q"; Value = "25"},
[PSCustomObject]@{TableInExistingHeaderWillBeRemoved = "O"; Value = "24"}
))
$prevTools = $prevInstalledSoftware.AddHeader("Tools")
$prevTools.AddHeader("HeaderWillBeRemoved").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "Z"; Value = "30"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "W"; Value = "29"}
))
# Next report
$nextSoftwareReport = [SoftwareReport]::new("macOS 11")
$nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.1")
$nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software")
$nextInstalledSoftware.AddHeader("HeaderWillExist")
$nextTools = $nextInstalledSoftware.AddHeader("Tools")
$nextTools.AddToolVersion("ToolWillBeAdded", "3.0.1")
$nextTools.AddTable(@(
[PSCustomObject]@{NewTableInExistingHeader = "A"; Value = "1"},
[PSCustomObject]@{NewTableInExistingHeader = "B"; Value = "2"}
))
$nextTools.AddHeader("NewHeaderWithTable").AddTable(@(
[PSCustomObject]@{NewTableInNewHeader = "C"; Value = "3"},
[PSCustomObject]@{NewTableInNewHeader = "D"; Value = "4"}
))
# Compare reports
$comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport)
$comparer.CompareReports()
$comparer.GetMarkdownReport() | Should -BeExactly @'
# :desktop_computer: Actions Runner Image: macOS 11
- Image Version: 20220922.1
## :mega: What's changed?
### Added :heavy_plus_sign:
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Current (20220922.1)</th>
</thead>
<tbody>
<tr>
<td rowspan="1">Tools</td>
<td>ToolWillBeAdded</td>
<td>3.0.1</td>
</tr>
</tbody>
</table>
#### Tools
| NewTableInExistingHeader | Value |
| ------------------------ | ----- |
| A | 1 |
| B | 2 |
#### Tools > NewHeaderWithTable
| NewTableInNewHeader | Value |
| ------------------- | ----- |
| C | 3 |
| D | 4 |
### Deleted :heavy_minus_sign:
#### HeaderWillExist
| TableInExistingHeaderWillBeRemoved | Value |
| ---------------------------------- | ------ |
| ~~Q~~ | ~~25~~ |
| ~~O~~ | ~~24~~ |
#### Tools > HeaderWillBeRemoved
| TableWillBeRemovedWithHeader | Value |
| ---------------------------- | ------ |
| ~~Z~~ | ~~30~~ |
| ~~W~~ | ~~29~~ |
'@
}
It "Tables are changed" {
# Previous report
$prevSoftwareReport = [SoftwareReport]::new("macOS 11")
$prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1")
$prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software")
$prevTools = $prevInstalledSoftware.AddHeader("Tools")
$prevTools.AddHeader("TableWithAddedRows").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "AA"; Value = "10"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "AB"; Value = "11"}
))
$prevTools.AddHeader("TableWithRemovedRows").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "BA"; Value = "32"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "BB"; Value = "33"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "BC"; Value = "34"}
))
$prevTools.AddHeader("TableWithUpdatedRow").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "CA"; Value = "42"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "CB"; Value = "43"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "CC"; Value = "44"}
))
$prevTools.AddHeader("TableWithUpdatedRows").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "DA"; Value = "50"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "DB"; Value = "51"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "DC"; Value = "52"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "DD"; Value = "53"}
))
$prevTools.AddHeader("TableWithComplexChanges").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "EA"; Value = "62"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "EB"; Value = "63"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "EC"; Value = "64"}
[PSCustomObject]@{TableWillBeRemovedWithHeader = "ED"; Value = "65"}
))
$prevTools.AddHeader("TableWithOnlyHeaderChanged").AddTable(@(
[PSCustomObject]@{TableWithOnlyHeaderChanged = "FA"; Value = "72"},
[PSCustomObject]@{TableWithOnlyHeaderChanged = "FB"; Value = "73"}
))
$prevTools.AddHeader("TableWithHeaderAndRowsChanges").AddTable(@(
[PSCustomObject]@{TableWithHeaderAndRowsChanges = "GA"; Value = "82"},
[PSCustomObject]@{TableWithHeaderAndRowsChanges = "GB"; Value = "83"},
[PSCustomObject]@{TableWithHeaderAndRowsChanges = "GC"; Value = "84"}
))
# Next report
$nextSoftwareReport = [SoftwareReport]::new("macOS 11")
$nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.1")
$nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software")
$nextTools = $nextInstalledSoftware.AddHeader("Tools")
$nextTools.AddHeader("TableWithAddedRows").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "AA"; Value = "10"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "AB"; Value = "11"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "AC"; Value = "12"}
))
$nextTools.AddHeader("TableWithRemovedRows").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "BB"; Value = "33"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "BC"; Value = "34"}
))
$nextTools.AddHeader("TableWithUpdatedRow").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "CA"; Value = "42"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "CB"; Value = "500"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "CC"; Value = "44"}
))
$nextTools.AddHeader("TableWithUpdatedRows").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "DA"; Value = "50"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "DB"; Value = "5100"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "DC"; Value = "5200"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "DD"; Value = "53"}
))
$nextTools.AddHeader("TableWithComplexChanges").AddTable(@(
[PSCustomObject]@{TableWillBeRemovedWithHeader = "EB"; Value = "63"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "EC"; Value = "640"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "ED"; Value = "65"},
[PSCustomObject]@{TableWillBeRemovedWithHeader = "EE"; Value = "66"}
))
$nextTools.AddHeader("TableWithOnlyHeaderChanged").AddTable(@(
[PSCustomObject]@{TableWithOnlyHeaderChanged2 = "FA"; Value = "72"},
[PSCustomObject]@{TableWithOnlyHeaderChanged2 = "FB"; Value = "73"}
))
$nextTools.AddHeader("TableWithHeaderAndRowsChanges").AddTable(@(
[PSCustomObject]@{TableWithHeaderAndRowsChanges2 = "GA"; Value = "82"},
[PSCustomObject]@{TableWithHeaderAndRowsChanges2 = "GE"; Value = "850"},
[PSCustomObject]@{TableWithHeaderAndRowsChanges2 = "GC"; Value = "840"}
))
# Compare reports
$comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport)
$comparer.CompareReports()
$comparer.GetMarkdownReport() | Should -BeExactly @'
# :desktop_computer: Actions Runner Image: macOS 11
- Image Version: 20220922.1
## :mega: What's changed?
### Added :heavy_plus_sign:
#### Tools > TableWithAddedRows
| TableWillBeRemovedWithHeader | Value |
| ---------------------------- | ----- |
| AC | 12 |
#### Tools > TableWithHeaderAndRowsChanges
| TableWithHeaderAndRowsChanges2 | Value |
| ------------------------------ | ----- |
| GA | 82 |
| GE | 850 |
| GC | 840 |
### Deleted :heavy_minus_sign:
#### Tools > TableWithRemovedRows
| TableWillBeRemovedWithHeader | Value |
| ---------------------------- | ------ |
| ~~BA~~ | ~~32~~ |
#### Tools > TableWithHeaderAndRowsChanges
| TableWithHeaderAndRowsChanges | Value |
| ----------------------------- | ------ |
| ~~GA~~ | ~~82~~ |
| ~~GB~~ | ~~83~~ |
| ~~GC~~ | ~~84~~ |
### Updated
#### Tools > TableWithUpdatedRow
| TableWillBeRemovedWithHeader | Value |
| ---------------------------- | ------ |
| ~~CB~~ | ~~43~~ |
| CB | 500 |
#### Tools > TableWithUpdatedRows
| TableWillBeRemovedWithHeader | Value |
| ---------------------------- | ------ |
| ~~DB~~ | ~~51~~ |
| ~~DC~~ | ~~52~~ |
| DB | 5100 |
| DC | 5200 |
#### Tools > TableWithComplexChanges
| TableWillBeRemovedWithHeader | Value |
| ---------------------------- | ------ |
| ~~EA~~ | ~~62~~ |
| ~~EC~~ | ~~64~~ |
| EC | 640 |
| EE | 66 |
'@
}
It "Reports are identical" {
# Previous report
$prevSoftwareReport = [SoftwareReport]::new("macOS 11")
$prevSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.1 (20G817)")
$prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1")
$prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software")
$prevTools = $prevInstalledSoftware.AddHeader("Tools")
$prevTools.AddToolVersion("ToolA", "1.0.0")
$prevTools.AddToolVersion("ToolB", "3.0.1")
# Next report
$nextSoftwareReport = [SoftwareReport]::new("macOS 11")
$nextSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.1 (20G817)")
$nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.1")
$nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software")
$nextTools = $nextInstalledSoftware.AddHeader("Tools")
$nextTools.AddToolVersion("ToolA", "1.0.0")
$nextTools.AddToolVersion("ToolB", "3.0.1")
# Compare reports
$comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport)
$comparer.CompareReports()
$comparer.GetMarkdownReport() | Should -BeExactly @'
# :desktop_computer: Actions Runner Image: macOS 11
- OS Version: macOS 11.7.1 (20G817)
- Image Version: 20220922.1
## :mega: What's changed?
'@
}
}
================================================
FILE: helpers/software-report-base/tests/SoftwareReport.DifferenceCalculator.Unit.Tests.ps1
================================================
using module ../SoftwareReport.Nodes.psm1
using module ../SoftwareReport.DifferenceCalculator.psm1
BeforeDiscovery {
Import-Module $(Join-Path $PSScriptRoot "TestHelpers.psm1") -DisableNameChecking
}
Describe "Comparer.UnitTests" {
Describe "Headers Tree" {
It "Add Node to existing header" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersion("MyTool1", "2.1.3")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 1
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 0
$comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty
$comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode])
$comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.AddedItems[0].CurrentReportNode.Version | Should -Be "2.1.3"
$comparer.AddedItems[0].Headers | Should -BeArray @("MyHeader")
}
It "Add new header with Node" {
$prevReport = [HeaderNode]::new("Version 1")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddHeader("MySubHeader").AddToolVersion("MyTool1", "2.1.3")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 1
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 0
$comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty
$comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode])
$comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.AddedItems[0].CurrentReportNode.Version | Should -Be "2.1.3"
$comparer.AddedItems[0].Headers | Should -BeArray @("MyHeader", "MySubHeader")
}
It "Remove Node from existing header" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersion("MyTool1", "2.1.3")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 0
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 1
$comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode])
$comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.DeletedItems[0].PreviousReportNode.Version | Should -Be "2.1.3"
$comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty
$comparer.DeletedItems[0].Headers | Should -BeArray @("MyHeader")
}
It "Remove header with Node" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3")
$nextReport = [HeaderNode]::new("Version 2")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 0
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 1
$comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode])
$comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.DeletedItems[0].PreviousReportNode.Version | Should -Be "2.1.3"
$comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty
$comparer.DeletedItems[0].Headers | Should -BeArray @("MyHeader", "MySubheader")
}
It "Node with minor changes" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.4")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 0
$comparer.ChangedItems | Should -HaveCount 1
$comparer.DeletedItems | Should -HaveCount 0
$comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode])
$comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].PreviousReportNode.Version | Should -Be "2.1.3"
$comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode])
$comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].CurrentReportNode.Version | Should -Be "2.1.4"
$comparer.ChangedItems[0].Headers | Should -BeArray @("MyHeader", "MySubHeader")
}
It "Node without changes" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 0
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 0
}
It "Node is moved to different header" {
$prevReport = [HeaderNode]::new("Version 1")
$prevRep
gitextract_ol0xcjy2/
├── .gitattributes
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── announcement.yml
│ │ ├── bug-report.yml
│ │ ├── config.yml
│ │ └── tool-request.yml
│ ├── copilot-instructions.md
│ ├── pull_request_template.md
│ └── workflows/
│ ├── check-pinned-versions.yml
│ ├── codeql-analysis.yml
│ ├── create_github_release.yml
│ ├── create_pull_request.yml
│ ├── create_sbom_report.yml
│ ├── docker-images.yml
│ ├── linter.yml
│ ├── merge_pull_request.yml
│ ├── powershell-tests.yml
│ ├── trigger-ubuntu-win-build.yml
│ ├── ubuntu2204.yml
│ ├── ubuntu2404.yml
│ ├── update_github_release.yml
│ ├── validate-json-schema.yml
│ ├── windows2022.yml
│ ├── windows2025-vs2026.yml
│ └── windows2025.yml
├── .gitignore
├── .vscode/
│ ├── extensions.json
│ ├── settings.json
│ └── tasks.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── docs/
│ ├── create-image-and-azure-resources.md
│ └── dotnet-ubuntu.md
├── helpers/
│ ├── CheckJsonSchema.ps1
│ ├── CheckOutdatedVersionPinning.ps1
│ ├── CreateAzureVMFromPackerTemplate.ps1
│ ├── GenerateResourcesAndImage.ps1
│ ├── GitHubApi.psm1
│ ├── WaitWorkflowCompletion.ps1
│ └── software-report-base/
│ ├── Calculate-ImagesDifference.ps1
│ ├── SoftwareReport.BaseNodes.psm1
│ ├── SoftwareReport.DifferenceCalculator.psm1
│ ├── SoftwareReport.DifferenceRender.psm1
│ ├── SoftwareReport.Nodes.psm1
│ ├── SoftwareReport.psm1
│ └── tests/
│ ├── SoftwareReport.Difference.E2E.Tests.ps1
│ ├── SoftwareReport.DifferenceCalculator.Unit.Tests.ps1
│ ├── SoftwareReport.DifferenceRender.Unit.Tests.ps1
│ ├── SoftwareReport.E2E.Tests.ps1
│ ├── SoftwareReport.Nodes.Unit.Tests.ps1
│ └── TestHelpers.psm1
├── images/
│ ├── macos/
│ │ ├── assets/
│ │ │ ├── add-certificate.swift
│ │ │ ├── auto-software-update-arm64.exp
│ │ │ ├── bashprofile
│ │ │ ├── bashrc
│ │ │ └── bootstrap-provisioner/
│ │ │ ├── change_password
│ │ │ ├── installNewProvisioner.sh
│ │ │ ├── kcpassword.py
│ │ │ └── setAutoLogin.sh
│ │ ├── macos-14-Readme.md
│ │ ├── macos-14-arm64-Readme.md
│ │ ├── macos-15-Readme.md
│ │ ├── macos-15-arm64-Readme.md
│ │ ├── macos-26-Readme.md
│ │ ├── macos-26-arm64-Readme.md
│ │ ├── scripts/
│ │ │ ├── build/
│ │ │ │ ├── Configure-Toolset.ps1
│ │ │ │ ├── Configure-Xcode-Simulators.ps1
│ │ │ │ ├── Install-Toolset.ps1
│ │ │ │ ├── Install-Xcode.ps1
│ │ │ │ ├── Update-XcodeSimulators.ps1
│ │ │ │ ├── configure-auto-updates.sh
│ │ │ │ ├── configure-autologin.sh
│ │ │ │ ├── configure-hostname.sh
│ │ │ │ ├── configure-machine.sh
│ │ │ │ ├── configure-ntpconf.sh
│ │ │ │ ├── configure-preimagedata.sh
│ │ │ │ ├── configure-shell.sh
│ │ │ │ ├── configure-ssh.sh
│ │ │ │ ├── configure-system.sh
│ │ │ │ ├── configure-tccdb-macos.sh
│ │ │ │ ├── configure-windows.sh
│ │ │ │ ├── configure-xcode.sh
│ │ │ │ ├── install-actions-cache.sh
│ │ │ │ ├── install-android-sdk.sh
│ │ │ │ ├── install-audiodevice.sh
│ │ │ │ ├── install-aws-tools.sh
│ │ │ │ ├── install-azcopy.sh
│ │ │ │ ├── install-bicep.sh
│ │ │ │ ├── install-chrome.sh
│ │ │ │ ├── install-cocoapods.sh
│ │ │ │ ├── install-codeql-bundle.sh
│ │ │ │ ├── install-common-utils.sh
│ │ │ │ ├── install-dotnet.sh
│ │ │ │ ├── install-edge.sh
│ │ │ │ ├── install-firefox.sh
│ │ │ │ ├── install-gcc.sh
│ │ │ │ ├── install-git.sh
│ │ │ │ ├── install-homebrew.sh
│ │ │ │ ├── install-llvm.sh
│ │ │ │ ├── install-mono.sh
│ │ │ │ ├── install-nginx.sh
│ │ │ │ ├── install-node.sh
│ │ │ │ ├── install-openjdk.sh
│ │ │ │ ├── install-openssl.sh
│ │ │ │ ├── install-php.sh
│ │ │ │ ├── install-postgresql.sh
│ │ │ │ ├── install-powershell.sh
│ │ │ │ ├── install-python.sh
│ │ │ │ ├── install-rosetta.sh
│ │ │ │ ├── install-ruby.sh
│ │ │ │ ├── install-rubygems.sh
│ │ │ │ ├── install-rust.sh
│ │ │ │ ├── install-safari.sh
│ │ │ │ ├── install-swiftlint.sh
│ │ │ │ ├── install-unxip.sh
│ │ │ │ ├── install-vcpkg.sh
│ │ │ │ └── install-xcode-clt.sh
│ │ │ ├── docs-gen/
│ │ │ │ ├── Generate-SoftwareReport.ps1
│ │ │ │ ├── SoftwareReport.Android.psm1
│ │ │ │ ├── SoftwareReport.Browsers.psm1
│ │ │ │ ├── SoftwareReport.Common.psm1
│ │ │ │ ├── SoftwareReport.Helpers.psm1
│ │ │ │ ├── SoftwareReport.Java.psm1
│ │ │ │ ├── SoftwareReport.Toolcache.psm1
│ │ │ │ └── SoftwareReport.Xcode.psm1
│ │ │ ├── helpers/
│ │ │ │ ├── Common.Helpers.psm1
│ │ │ │ ├── Xcode.Helpers.psm1
│ │ │ │ ├── Xcode.Installer.psm1
│ │ │ │ ├── confirm-identified-developers-macos14.scpt
│ │ │ │ ├── confirm-identified-developers-macos15.scpt
│ │ │ │ ├── invoke-tests.sh
│ │ │ │ └── utils.sh
│ │ │ └── tests/
│ │ │ ├── ActionArchiveCache.Tests.ps1
│ │ │ ├── Android.Tests.ps1
│ │ │ ├── BasicTools.Tests.ps1
│ │ │ ├── Browsers.Tests.ps1
│ │ │ ├── Common.Tests.ps1
│ │ │ ├── Git.Tests.ps1
│ │ │ ├── Helpers.psm1
│ │ │ ├── Java.Tests.ps1
│ │ │ ├── LLVM.Tests.ps1
│ │ │ ├── Linters.Tests.ps1
│ │ │ ├── Mono.Tests.ps1
│ │ │ ├── Node.Tests.ps1
│ │ │ ├── OpenSSL.Tests.ps1
│ │ │ ├── PHP.Tests.ps1
│ │ │ ├── Powershell.Tests.ps1
│ │ │ ├── Python.Tests.ps1
│ │ │ ├── Rosetta.Tests.ps1
│ │ │ ├── Ruby.Tests.ps1
│ │ │ ├── RubyGem.Tests.ps1
│ │ │ ├── RunAll-Tests.ps1
│ │ │ ├── Rust.Tests.ps1
│ │ │ ├── System.Tests.ps1
│ │ │ ├── Toolcache.Tests.ps1
│ │ │ ├── Toolset.Tests.ps1
│ │ │ └── Xcode.Tests.ps1
│ │ ├── templates/
│ │ │ ├── macOS-14.anka.pkr.hcl
│ │ │ ├── macOS-14.arm64.anka.pkr.hcl
│ │ │ ├── macOS-15.anka.pkr.hcl
│ │ │ ├── macOS-15.arm64.anka.pkr.hcl
│ │ │ ├── macOS-26.anka.pkr.hcl
│ │ │ └── macOS-26.arm64.anka.pkr.hcl
│ │ └── toolsets/
│ │ ├── Readme.md
│ │ ├── toolset-14.json
│ │ ├── toolset-15.json
│ │ └── toolset-26.json
│ ├── ubuntu/
│ │ ├── Ubuntu2204-Readme.md
│ │ ├── Ubuntu2404-Readme.md
│ │ ├── assets/
│ │ │ ├── post-gen/
│ │ │ │ ├── cleanup-logs.sh
│ │ │ │ ├── environment-variables.sh
│ │ │ │ └── systemd-linger.sh
│ │ │ └── ubuntu2204.conf
│ │ ├── scripts/
│ │ │ ├── build/
│ │ │ │ ├── Configure-Toolset.ps1
│ │ │ │ ├── Install-PowerShellAzModules.ps1
│ │ │ │ ├── Install-PowerShellModules.ps1
│ │ │ │ ├── Install-Toolset.ps1
│ │ │ │ ├── cleanup.sh
│ │ │ │ ├── configure-apt-mock.sh
│ │ │ │ ├── configure-apt-sources.sh
│ │ │ │ ├── configure-apt.sh
│ │ │ │ ├── configure-dpkg.sh
│ │ │ │ ├── configure-environment.sh
│ │ │ │ ├── configure-image-data.sh
│ │ │ │ ├── configure-limits.sh
│ │ │ │ ├── configure-snap.sh
│ │ │ │ ├── configure-system.sh
│ │ │ │ ├── install-actions-cache.sh
│ │ │ │ ├── install-aliyun-cli.sh
│ │ │ │ ├── install-android-sdk.sh
│ │ │ │ ├── install-apache.sh
│ │ │ │ ├── install-apt-common.sh
│ │ │ │ ├── install-apt-vital.sh
│ │ │ │ ├── install-aws-tools.sh
│ │ │ │ ├── install-azcopy.sh
│ │ │ │ ├── install-azure-cli.sh
│ │ │ │ ├── install-azure-devops-cli.sh
│ │ │ │ ├── install-bazel.sh
│ │ │ │ ├── install-bicep.sh
│ │ │ │ ├── install-clang.sh
│ │ │ │ ├── install-cmake.sh
│ │ │ │ ├── install-codeql-bundle.sh
│ │ │ │ ├── install-container-tools.sh
│ │ │ │ ├── install-docker.sh
│ │ │ │ ├── install-dotnetcore-sdk.sh
│ │ │ │ ├── install-firefox.sh
│ │ │ │ ├── install-gcc-compilers.sh
│ │ │ │ ├── install-gfortran.sh
│ │ │ │ ├── install-git-lfs.sh
│ │ │ │ ├── install-git.sh
│ │ │ │ ├── install-github-cli.sh
│ │ │ │ ├── install-google-chrome.sh
│ │ │ │ ├── install-google-cloud-cli.sh
│ │ │ │ ├── install-haskell.sh
│ │ │ │ ├── install-heroku.sh
│ │ │ │ ├── install-homebrew.sh
│ │ │ │ ├── install-java-tools.sh
│ │ │ │ ├── install-julia.sh
│ │ │ │ ├── install-kotlin.sh
│ │ │ │ ├── install-kubernetes-tools.sh
│ │ │ │ ├── install-leiningen.sh
│ │ │ │ ├── install-microsoft-edge.sh
│ │ │ │ ├── install-miniconda.sh
│ │ │ │ ├── install-mono.sh
│ │ │ │ ├── install-ms-repos.sh
│ │ │ │ ├── install-mssql-tools.sh
│ │ │ │ ├── install-mysql.sh
│ │ │ │ ├── install-nginx.sh
│ │ │ │ ├── install-ninja.sh
│ │ │ │ ├── install-nodejs.sh
│ │ │ │ ├── install-nvm.sh
│ │ │ │ ├── install-oc-cli.sh
│ │ │ │ ├── install-oras-cli.sh
│ │ │ │ ├── install-packer.sh
│ │ │ │ ├── install-php.sh
│ │ │ │ ├── install-pipx-packages.sh
│ │ │ │ ├── install-postgresql.sh
│ │ │ │ ├── install-powershell.sh
│ │ │ │ ├── install-pulumi.sh
│ │ │ │ ├── install-pypy.sh
│ │ │ │ ├── install-python.sh
│ │ │ │ ├── install-rlang.sh
│ │ │ │ ├── install-ruby.sh
│ │ │ │ ├── install-rust.sh
│ │ │ │ ├── install-sbt.sh
│ │ │ │ ├── install-selenium.sh
│ │ │ │ ├── install-sqlpackage.sh
│ │ │ │ ├── install-swift.sh
│ │ │ │ ├── install-terraform.sh
│ │ │ │ ├── install-vcpkg.sh
│ │ │ │ ├── install-yq.sh
│ │ │ │ ├── install-zstd.sh
│ │ │ │ ├── list-dpkg.sh
│ │ │ │ └── post-build-validation.sh
│ │ │ ├── docs-gen/
│ │ │ │ ├── Generate-SoftwareReport.ps1
│ │ │ │ ├── SoftwareReport.Android.psm1
│ │ │ │ ├── SoftwareReport.Browsers.psm1
│ │ │ │ ├── SoftwareReport.CachedTools.psm1
│ │ │ │ ├── SoftwareReport.Common.psm1
│ │ │ │ ├── SoftwareReport.Databases.psm1
│ │ │ │ ├── SoftwareReport.Helpers.psm1
│ │ │ │ ├── SoftwareReport.Java.psm1
│ │ │ │ ├── SoftwareReport.Rust.psm1
│ │ │ │ ├── SoftwareReport.Tools.psm1
│ │ │ │ └── SoftwareReport.WebServers.psm1
│ │ │ ├── helpers/
│ │ │ │ ├── Common.Helpers.psm1
│ │ │ │ ├── etc-environment.sh
│ │ │ │ ├── install.sh
│ │ │ │ ├── invoke-tests.sh
│ │ │ │ └── os.sh
│ │ │ └── tests/
│ │ │ ├── ActionArchiveCache.Tests.ps1
│ │ │ ├── Android.Tests.ps1
│ │ │ ├── Apt.Tests.ps1
│ │ │ ├── Browsers.Tests.ps1
│ │ │ ├── CLI.Tools.Tests.ps1
│ │ │ ├── Common.Tests.ps1
│ │ │ ├── Databases.Tests.ps1
│ │ │ ├── DotnetSDK.Tests.ps1
│ │ │ ├── Haskell.Tests.ps1
│ │ │ ├── Helpers.psm1
│ │ │ ├── Java.Tests.ps1
│ │ │ ├── Node.Tests.ps1
│ │ │ ├── PowerShellModules.Tests.ps1
│ │ │ ├── RunAll-Tests.ps1
│ │ │ ├── System.Tests.ps1
│ │ │ ├── Tools.Tests.ps1
│ │ │ ├── Toolset.Tests.ps1
│ │ │ └── WebServers.Tests.ps1
│ │ ├── templates/
│ │ │ ├── build.ubuntu-22_04.pkr.hcl
│ │ │ ├── build.ubuntu-24_04.pkr.hcl
│ │ │ ├── locals.ubuntu.pkr.hcl
│ │ │ ├── source.ubuntu.pkr.hcl
│ │ │ └── variable.ubuntu.pkr.hcl
│ │ └── toolsets/
│ │ ├── toolset-2204.json
│ │ └── toolset-2404.json
│ ├── ubuntu-slim/
│ │ ├── Dockerfile
│ │ ├── generate-software-report.sh
│ │ ├── scripts/
│ │ │ ├── build/
│ │ │ │ ├── configure-apt-sources.sh
│ │ │ │ ├── configure-apt.sh
│ │ │ │ ├── configure-dpkg.sh
│ │ │ │ ├── configure-environment.sh
│ │ │ │ ├── configure-image-data-file.sh
│ │ │ │ ├── configure-system.sh
│ │ │ │ ├── install-actions-cache.sh
│ │ │ │ ├── install-apt-common.sh
│ │ │ │ ├── install-apt-vital.sh
│ │ │ │ ├── install-aws-tools.sh
│ │ │ │ ├── install-azcopy.sh
│ │ │ │ ├── install-azure-cli.sh
│ │ │ │ ├── install-azure-devops-cli.sh
│ │ │ │ ├── install-bicep.sh
│ │ │ │ ├── install-docker-cli.sh
│ │ │ │ ├── install-git-lfs.sh
│ │ │ │ ├── install-git.sh
│ │ │ │ ├── install-github-cli.sh
│ │ │ │ ├── install-google-cloud-cli.sh
│ │ │ │ ├── install-ms-repos.sh
│ │ │ │ ├── install-nodejs.sh
│ │ │ │ ├── install-nvm.sh
│ │ │ │ ├── install-pipx-packages.sh
│ │ │ │ ├── install-powershell.sh
│ │ │ │ ├── install-python.sh
│ │ │ │ ├── install-yq.sh
│ │ │ │ └── install-zstd.sh
│ │ │ ├── docs-gen/
│ │ │ │ ├── Common.Helpers.psm1
│ │ │ │ ├── Generate-SoftwareReport.ps1
│ │ │ │ ├── SoftwareReport.Common.psm1
│ │ │ │ ├── SoftwareReport.Helpers.psm1
│ │ │ │ └── SoftwareReport.Tools.psm1
│ │ │ ├── entrypoint.sh
│ │ │ └── helpers/
│ │ │ ├── cleanup.sh
│ │ │ ├── etc-environment.sh
│ │ │ ├── install.sh
│ │ │ └── os.sh
│ │ ├── test.sh
│ │ ├── toolsets/
│ │ │ └── toolset.json
│ │ ├── ubuntu-slim-Readme.md
│ │ └── ubuntu-slim-Report.json
│ └── windows/
│ ├── Windows2022-Readme.md
│ ├── Windows2025-Readme.md
│ ├── Windows2025-VS2026-Readme.md
│ ├── assets/
│ │ └── post-gen/
│ │ ├── GenerateIISExpressCertificate.ps1
│ │ ├── InternetExplorerConfiguration.ps1
│ │ ├── Msys2FirstLaunch.ps1
│ │ ├── VSConfiguration.ps1
│ │ └── warmup.vdproj
│ ├── scripts/
│ │ ├── build/
│ │ │ ├── Configure-BaseImage.ps1
│ │ │ ├── Configure-DeveloperMode.ps1
│ │ │ ├── Configure-Diagnostics.ps1
│ │ │ ├── Configure-DotnetSecureChannel.ps1
│ │ │ ├── Configure-DynamicPort.ps1
│ │ │ ├── Configure-GDIProcessHandleQuota.ps1
│ │ │ ├── Configure-ImageDataFile.ps1
│ │ │ ├── Configure-PowerShell.ps1
│ │ │ ├── Configure-Shell.ps1
│ │ │ ├── Configure-System.ps1
│ │ │ ├── Configure-SystemEnvironment.ps1
│ │ │ ├── Configure-Toolset.ps1
│ │ │ ├── Configure-User.ps1
│ │ │ ├── Configure-WindowsDefender.ps1
│ │ │ ├── Install-AWSTools.ps1
│ │ │ ├── Install-ActionsCache.ps1
│ │ │ ├── Install-AliyunCli.ps1
│ │ │ ├── Install-AndroidSDK.ps1
│ │ │ ├── Install-Apache.ps1
│ │ │ ├── Install-AzureCli.ps1
│ │ │ ├── Install-AzureCosmosDbEmulator.ps1
│ │ │ ├── Install-AzureDevOpsCli.ps1
│ │ │ ├── Install-Bazel.ps1
│ │ │ ├── Install-Chocolatey.ps1
│ │ │ ├── Install-ChocolateyPackages.ps1
│ │ │ ├── Install-Chrome.ps1
│ │ │ ├── Install-CodeQLBundle.ps1
│ │ │ ├── Install-DACFx.ps1
│ │ │ ├── Install-Docker.ps1
│ │ │ ├── Install-DockerCompose.ps1
│ │ │ ├── Install-DockerWinCred.ps1
│ │ │ ├── Install-DotnetSDK.ps1
│ │ │ ├── Install-EdgeDriver.ps1
│ │ │ ├── Install-Firefox.ps1
│ │ │ ├── Install-Git.ps1
│ │ │ ├── Install-GitHub-CLI.ps1
│ │ │ ├── Install-Haskell.ps1
│ │ │ ├── Install-IEWebDriver.ps1
│ │ │ ├── Install-JavaTools.ps1
│ │ │ ├── Install-Kotlin.ps1
│ │ │ ├── Install-KubernetesTools.ps1
│ │ │ ├── Install-LLVM.ps1
│ │ │ ├── Install-Mercurial.ps1
│ │ │ ├── Install-Mingw64.ps1
│ │ │ ├── Install-Miniconda.ps1
│ │ │ ├── Install-MongoDB.ps1
│ │ │ ├── Install-Msys2.ps1
│ │ │ ├── Install-MysqlCli.ps1
│ │ │ ├── Install-NSIS.ps1
│ │ │ ├── Install-NativeImages.ps1
│ │ │ ├── Install-Nginx.ps1
│ │ │ ├── Install-NodeJS.ps1
│ │ │ ├── Install-OpenSSL.ps1
│ │ │ ├── Install-PHP.ps1
│ │ │ ├── Install-Pipx.ps1
│ │ │ ├── Install-PostgreSQL.ps1
│ │ │ ├── Install-PowerShellModules.ps1
│ │ │ ├── Install-PowershellAzModules.ps1
│ │ │ ├── Install-PowershellCore.ps1
│ │ │ ├── Install-PyPy.ps1
│ │ │ ├── Install-R.ps1
│ │ │ ├── Install-RootCA.ps1
│ │ │ ├── Install-Ruby.ps1
│ │ │ ├── Install-Rust.ps1
│ │ │ ├── Install-SQLOLEDBDriver.ps1
│ │ │ ├── Install-SQLPowerShellTools.ps1
│ │ │ ├── Install-Sbt.ps1
│ │ │ ├── Install-Selenium.ps1
│ │ │ ├── Install-ServiceFabricSDK.ps1
│ │ │ ├── Install-Stack.ps1
│ │ │ ├── Install-Toolset.ps1
│ │ │ ├── Install-TortoiseSvn.ps1
│ │ │ ├── Install-VSExtensions.ps1
│ │ │ ├── Install-Vcpkg.ps1
│ │ │ ├── Install-VisualStudio.ps1
│ │ │ ├── Install-WDK.ps1
│ │ │ ├── Install-WSL2.ps1
│ │ │ ├── Install-WebPlatformInstaller.ps1
│ │ │ ├── Install-WinAppDriver.ps1
│ │ │ ├── Install-WindowsFeatures.ps1
│ │ │ ├── Install-WindowsUpdates.ps1
│ │ │ ├── Install-WindowsUpdatesAfterReboot.ps1
│ │ │ ├── Install-Wix.ps1
│ │ │ ├── Install-Zstd.ps1
│ │ │ ├── Invoke-Cleanup.ps1
│ │ │ └── Post-Build-Validation.ps1
│ │ ├── docs-gen/
│ │ │ ├── Generate-SoftwareReport.ps1
│ │ │ ├── SoftwareReport.Android.psm1
│ │ │ ├── SoftwareReport.Browsers.psm1
│ │ │ ├── SoftwareReport.CachedTools.psm1
│ │ │ ├── SoftwareReport.Common.psm1
│ │ │ ├── SoftwareReport.Databases.psm1
│ │ │ ├── SoftwareReport.Helpers.psm1
│ │ │ ├── SoftwareReport.Java.psm1
│ │ │ ├── SoftwareReport.Tools.psm1
│ │ │ ├── SoftwareReport.VisualStudio.psm1
│ │ │ └── SoftwareReport.WebServers.psm1
│ │ ├── helpers/
│ │ │ ├── AndroidHelpers.ps1
│ │ │ ├── ChocoHelpers.ps1
│ │ │ ├── ImageHelpers.psd1
│ │ │ ├── ImageHelpers.psm1
│ │ │ ├── InstallHelpers.ps1
│ │ │ ├── PathHelpers.ps1
│ │ │ ├── VisualStudioHelpers.ps1
│ │ │ └── test/
│ │ │ └── ImageHelpers.Tests.ps1
│ │ └── tests/
│ │ ├── ActionArchiveCache.Tests.ps1
│ │ ├── Android.Tests.ps1
│ │ ├── Apache.Tests.ps1
│ │ ├── Browsers.Tests.ps1
│ │ ├── CLI.Tools.Tests.ps1
│ │ ├── ChocoPackages.Tests.ps1
│ │ ├── Databases.Tests.ps1
│ │ ├── Docker.Tests.ps1
│ │ ├── DotnetSDK.Tests.ps1
│ │ ├── Git.Tests.ps1
│ │ ├── Haskell.Tests.ps1
│ │ ├── Helpers.psm1
│ │ ├── Java.Tests.ps1
│ │ ├── LLVM.Tests.ps1
│ │ ├── MSYS2.Tests.ps1
│ │ ├── Miniconda.Tests.ps1
│ │ ├── Nginx.Tests.ps1
│ │ ├── Node.Tests.ps1
│ │ ├── PHP.Tests.ps1
│ │ ├── PipxPackages.Tests.ps1
│ │ ├── PowerShellAzModules.Tests.ps1
│ │ ├── PowerShellModules.Tests.ps1
│ │ ├── RunAll-Tests.ps1
│ │ ├── Rust.Tests.ps1
│ │ ├── SSDTExtensions.Tests.ps1
│ │ ├── Shell.Tests.ps1
│ │ ├── Tools.Tests.ps1
│ │ ├── Toolset.Tests.ps1
│ │ ├── VisualStudio.Tests.ps1
│ │ ├── Vsix.Tests.ps1
│ │ ├── WDK.Tests.ps1
│ │ ├── WinAppDriver.Tests.ps1
│ │ ├── WindowsFeatures.Tests.ps1
│ │ └── Wix.Tests.ps1
│ ├── templates/
│ │ ├── build.windows-2022.pkr.hcl
│ │ ├── build.windows-2025-vs2026.pkr.hcl
│ │ ├── build.windows-2025.pkr.hcl
│ │ ├── locals.windows.pkr.hcl
│ │ ├── source.windows.pkr.hcl
│ │ └── variable.windows.pkr.hcl
│ └── toolsets/
│ ├── toolset-2022.json
│ ├── toolset-2025-vs2026.json
│ └── toolset-2025.json
├── images.CI/
│ ├── credscan-exclusions.json
│ ├── linux-and-win/
│ │ ├── build-image.ps1
│ │ ├── cleanup.ps1
│ │ └── create-release.ps1
│ ├── measure-provisioners-duration.ps1
│ └── shebang-linter.ps1
└── schemas/
└── toolset-schema.json
SYMBOL INDEX (1 symbols across 1 files) FILE: images/macos/assets/bootstrap-provisioner/kcpassword.py function encode_data (line 12) | def encode_data(passwd):
Condensed preview — 504 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,728K chars).
[
{
"path": ".gitattributes",
"chars": 20,
"preview": "* text=auto eol=lf"
},
{
"path": ".github/CODEOWNERS",
"chars": 30,
"preview": "* @actions/runner-images-team\n"
},
{
"path": ".github/ISSUE_TEMPLATE/announcement.yml",
"chars": 1543,
"preview": "name: Announcement\ndescription: Submit an announcement\nlabels: [Announcement]\nbody:\n - type: textarea\n attributes:\n "
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report.yml",
"chars": 2285,
"preview": "name: Bug Report\ndescription: Submit a bug report.\nlabels: [bug report, needs triage]\nbody:\n - type: textarea\n attri"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 233,
"preview": "blank_issues_enabled: false\n\ncontact_links:\n - name: Get help in GitHub Discussions\n url: https://github.com/actions"
},
{
"path": ".github/ISSUE_TEMPLATE/tool-request.yml",
"chars": 2475,
"preview": "name: Tool request\ndescription: Request a new tool or update to a tool\ntitle: Update/Add [tool name]\nlabels: [feature re"
},
{
"path": ".github/copilot-instructions.md",
"chars": 2931,
"preview": "# GitHub Copilot Instructions for Actions Runner Images Repository\n\n## Scope and goals\n\n- This repository serves as the "
},
{
"path": ".github/pull_request_template.md",
"chars": 624,
"preview": "# Description\nNew tool, Bug fixing, or Improvement?\nPlease include a summary of the change and which issue is fixed. Als"
},
{
"path": ".github/workflows/check-pinned-versions.yml",
"chars": 457,
"preview": "name: Check Outdated Version Pinning\n\non:\n schedule:\n - cron: '0 12 * * 1' # Run at 12:00 UTC every Monday\n\npermiss"
},
{
"path": ".github/workflows/codeql-analysis.yml",
"chars": 2417,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/create_github_release.yml",
"chars": 688,
"preview": "name: Create GitHub release\n\non:\n repository_dispatch:\n types: [create-github-release]\n\n\njobs:\n Create_GitHub_relea"
},
{
"path": ".github/workflows/create_pull_request.yml",
"chars": 2275,
"preview": "name: Create Pull Request\n\non:\n repository_dispatch:\n types: [create-pr]\n\n\njobs:\n Create_pull_request:\n runs-on:"
},
{
"path": ".github/workflows/create_sbom_report.yml",
"chars": 4481,
"preview": "name: Create SBOM for the release\n\nrun-name: Collecting SBOM for ${{ github.event.client_payload.agentSpec || 'unknown i"
},
{
"path": ".github/workflows/docker-images.yml",
"chars": 584,
"preview": "name: Test Docker Images\n\non:\n push:\n branches:\n - main\n paths:\n - 'images/ubuntu-slim/**'\n - '.gi"
},
{
"path": ".github/workflows/linter.yml",
"chars": 799,
"preview": "# CI Validation\n\nname: Linter\n\non:\n pull_request:\n branches: [ main ]\n paths:\n - '**.json'\n - '**.md'\n "
},
{
"path": ".github/workflows/merge_pull_request.yml",
"chars": 1544,
"preview": "name: Merge pull request\n\non:\n repository_dispatch:\n types: [merge-pr]\n\n\njobs:\n Merge_pull_request:\n runs-on: ub"
},
{
"path": ".github/workflows/powershell-tests.yml",
"chars": 513,
"preview": "# CI Validation\n\nname: PowerShell Tests\n\non:\n pull_request:\n branches: [ main ]\n paths:\n - 'helpers/software"
},
{
"path": ".github/workflows/trigger-ubuntu-win-build.yml",
"chars": 4509,
"preview": "name: Trigger Build workflow\n\non:\n workflow_call:\n inputs:\n image_type:\n required: true\n type: st"
},
{
"path": ".github/workflows/ubuntu2204.yml",
"chars": 450,
"preview": "name: Trigger Ubuntu22.04 CI\nrun-name: Ubuntu22.04 - ${{ github.event.pull_request.title }}\n\non:\n pull_request_target:\n"
},
{
"path": ".github/workflows/ubuntu2404.yml",
"chars": 450,
"preview": "name: Trigger Ubuntu24.04 CI\nrun-name: Ubuntu24.04 - ${{ github.event.pull_request.title }}\n\non:\n pull_request_target:\n"
},
{
"path": ".github/workflows/update_github_release.yml",
"chars": 852,
"preview": "name: Update release\n\non:\n repository_dispatch:\n types: [update-github-release]\n\n\njobs:\n Update_GitHub_release:\n "
},
{
"path": ".github/workflows/validate-json-schema.yml",
"chars": 346,
"preview": "name: Validate JSON Schema\n\non:\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n\njobs:\n v"
},
{
"path": ".github/workflows/windows2022.yml",
"chars": 453,
"preview": "name: Trigger Windows22 CI\nrun-name: Windows2022 - ${{ github.event.pull_request.title }}\n\non:\n pull_request_target:\n "
},
{
"path": ".github/workflows/windows2025-vs2026.yml",
"chars": 501,
"preview": "name: Trigger Windows25 with VS 2026 CI\nrun-name: Windows2025 with VS 2026 - ${{ github.event.pull_request.title }}\n\non:"
},
{
"path": ".github/workflows/windows2025.yml",
"chars": 453,
"preview": "name: Trigger Windows25 CI\nrun-name: Windows2025 - ${{ github.event.pull_request.title }}\n\non:\n pull_request_target:\n "
},
{
"path": ".gitignore",
"chars": 6465,
"preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
},
{
"path": ".vscode/extensions.json",
"chars": 189,
"preview": "{\n \"recommendations\": [\n \"streetsidesoftware.code-spell-checker\",\n \"hashicorp.hcl\",\n \"davidanson.vscode-markdo"
},
{
"path": ".vscode/settings.json",
"chars": 1251,
"preview": "{\n \"files.trimFinalNewlines\": true,\n \"files.insertFinalNewline\": true,\n \"powershell.codeFormatting.addWhitespaceAroun"
},
{
"path": ".vscode/tasks.json",
"chars": 1562,
"preview": "// Available variables which can be used inside of strings.\n// ${workspaceRoot}: the root folder of the team\n// ${file}:"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3370,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 10769,
"preview": "# Contributing\n\n[fork]: https://github.com/actions/runner-images/fork\n[pr]: https://github.com//actions/runner-images/co"
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2026 GitHub\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "README.md",
"chars": 18652,
"preview": "# GitHub Actions Runner Images\n\n**Table of Contents**\n\n- [About](#about)\n- [Available Images](#available-images)\n- [Anno"
},
{
"path": "SECURITY.md",
"chars": 194,
"preview": "If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](https://hackero"
},
{
"path": "docs/create-image-and-azure-resources.md",
"chars": 17956,
"preview": "# GitHub Actions Runner Images\n\nThe runner-images project uses [Packer](https://www.packer.io/) to generate disk images "
},
{
"path": "docs/dotnet-ubuntu.md",
"chars": 2479,
"preview": "# Ubuntu .NET Core Versions\n\n.NET has changed the recommended install methods for Ubuntu from 2404.\n\nThis document gives"
},
{
"path": "helpers/CheckJsonSchema.ps1",
"chars": 1807,
"preview": "$ErrorActionPreference = 'Stop'\n\n# A JSON schema validator which supports outputting line numbers for errors\n# this allo"
},
{
"path": "helpers/CheckOutdatedVersionPinning.ps1",
"chars": 3230,
"preview": "$ErrorActionPreference = 'Stop'\n\n# Find all toolset JSON files\n$toolsetFiles = Get-ChildItem -Recurse -Filter \"toolset-*"
},
{
"path": "helpers/CreateAzureVMFromPackerTemplate.ps1",
"chars": 3875,
"preview": "Function CreateAzureVMFromPackerTemplate {\n <#\n .SYNOPSIS\n A helper function to deploy a VM from a "
},
{
"path": "helpers/GenerateResourcesAndImage.ps1",
"chars": 17904,
"preview": "$ErrorActionPreference = 'Stop'\n\nenum ImageType {\n Windows2022 = 1\n Windows2025 = 2\n Windows202"
},
{
"path": "helpers/GitHubApi.psm1",
"chars": 2872,
"preview": "class GithubApi\n{\n [string] $Repository\n [object] hidden $AuthHeader\n\n GithubApi(\n [string] $Repository,"
},
{
"path": "helpers/WaitWorkflowCompletion.ps1",
"chars": 1599,
"preview": "Param (\n [Parameter(Mandatory)]\n [string] $WorkflowRunId,\n [Parameter(Mandatory)]\n [string] $Repository,\n "
},
{
"path": "helpers/software-report-base/Calculate-ImagesDifference.ps1",
"chars": 2252,
"preview": "using module ./SoftwareReport.psm1\nusing module ./SoftwareReport.DifferenceCalculator.psm1\n\n<#\n.SYNOPSIS\n Calculates "
},
{
"path": "helpers/software-report-base/SoftwareReport.BaseNodes.psm1",
"chars": 1563,
"preview": "############################\n### Abstract base nodes ####\n############################\n\n# Abstract base class for all no"
},
{
"path": "helpers/software-report-base/SoftwareReport.DifferenceCalculator.psm1",
"chars": 8303,
"preview": "using module ./SoftwareReport.psm1\nusing module ./SoftwareReport.BaseNodes.psm1\nusing module ./SoftwareReport.Nodes.psm1"
},
{
"path": "helpers/software-report-base/SoftwareReport.DifferenceRender.psm1",
"chars": 9752,
"preview": "using module ./SoftwareReport.psm1\nusing module ./SoftwareReport.BaseNodes.psm1\nusing module ./SoftwareReport.Nodes.psm1"
},
{
"path": "helpers/software-report-base/SoftwareReport.Nodes.psm1",
"chars": 14322,
"preview": "using module ./SoftwareReport.BaseNodes.psm1\n\n#########################################\n### Nodes to describe image soft"
},
{
"path": "helpers/software-report-base/SoftwareReport.psm1",
"chars": 1012,
"preview": "using module ./SoftwareReport.BaseNodes.psm1\nusing module ./SoftwareReport.Nodes.psm1\n\nclass SoftwareReport {\n [Valid"
},
{
"path": "helpers/software-report-base/tests/SoftwareReport.Difference.E2E.Tests.ps1",
"chars": 21974,
"preview": "using module ../SoftwareReport.psm1\nusing module ../SoftwareReport.DifferenceCalculator.psm1\n\nDescribe \"Comparer.E2E\" {\n"
},
{
"path": "helpers/software-report-base/tests/SoftwareReport.DifferenceCalculator.Unit.Tests.ps1",
"chars": 33227,
"preview": "using module ../SoftwareReport.Nodes.psm1\nusing module ../SoftwareReport.DifferenceCalculator.psm1\n\nBeforeDiscovery {\n "
},
{
"path": "helpers/software-report-base/tests/SoftwareReport.DifferenceRender.Unit.Tests.ps1",
"chars": 9356,
"preview": "using module ../SoftwareReport.Nodes.psm1\nusing module ../SoftwareReport.DifferenceRender.psm1\n\nBeforeDiscovery {\n Im"
},
{
"path": "helpers/software-report-base/tests/SoftwareReport.E2E.Tests.ps1",
"chars": 3361,
"preview": "using module ../SoftwareReport.psm1\nusing module ../SoftwareReport.Nodes.psm1\n\nDescribe \"SoftwareReport.E2E\" {\n Conte"
},
{
"path": "helpers/software-report-base/tests/SoftwareReport.Nodes.Unit.Tests.ps1",
"chars": 26299,
"preview": "using module ../SoftwareReport.Nodes.psm1\n\nBeforeDiscovery {\n Import-Module $(Join-Path $PSScriptRoot \"TestHelpers.ps"
},
{
"path": "helpers/software-report-base/tests/TestHelpers.psm1",
"chars": 1107,
"preview": "function ShouldBeArray([Array] $ActualValue, [Array]$ExpectedValue, [Switch] $Negate, [String] $Because) {\n if ($Nega"
},
{
"path": "images/macos/assets/add-certificate.swift",
"chars": 2148,
"preview": "import Foundation\nimport Security\n\nlet certInfo: CFDictionary\n\nenum SecurityError: Error {\n case generalError\n}\n\nfunc"
},
{
"path": "images/macos/assets/auto-software-update-arm64.exp",
"chars": 174,
"preview": "#! /usr/bin/expect -f\n\nset timeout -1\nspawn sudo /usr/sbin/softwareupdate --restart --verbose --install \"MACOSUPDATE\"\nex"
},
{
"path": "images/macos/assets/bashprofile",
"chars": 45,
"preview": "[ -f $HOME/.bashrc ] && source $HOME/.bashrc\n"
},
{
"path": "images/macos/assets/bashrc",
"chars": 1200,
"preview": "export LC_CTYPE=en_US.UTF-8\nexport LC_ALL=en_US.UTF-8\nexport LANG=en_US.UTF-8\n\nexport ANDROID_HOME=${HOME}/Library/Andro"
},
{
"path": "images/macos/assets/bootstrap-provisioner/change_password",
"chars": 581,
"preview": "#!/bin/bash\nUSERNAME=\"$1\"\nOLD_PASSWD=\"$2\"\nNEW_PASSWD=\"$3\"\nUPDATE_LOGIN_KEYCHAIN=\"${4:-true}\"\n\nsudo /usr/sbin/sysadminctl"
},
{
"path": "images/macos/assets/bootstrap-provisioner/installNewProvisioner.sh",
"chars": 1194,
"preview": "#!/bin/bash -e -o pipefail\nBOOTSTRAP_PATH=\"$1\"\nProvisionerPackageUri=\"$2\"\nProvisionerScriptUri=\"$3\"\nScriptName=\"$4\"\nScri"
},
{
"path": "images/macos/assets/bootstrap-provisioner/kcpassword.py",
"chars": 2437,
"preview": "#!/usr/bin/env python3\n\n# Port of Gavin Brock's Perl kcpassword generator to Python, by Tom Taylor\n# <tom@tomtaylor.co.u"
},
{
"path": "images/macos/assets/bootstrap-provisioner/setAutoLogin.sh",
"chars": 3078,
"preview": "#!/bin/bash -e -o pipefail\n: <<-LICENSE_BLOCK\nsetAutoLogin (20210911) - Copyright (c) 2021 Joel Bruner (https://github.c"
},
{
"path": "images/macos/macos-14-Readme.md",
"chars": 22301,
"preview": "| Announcements |\n|-|\n| [macOS 26 (Tahoe) is now generally available in GitHub Actions](https://github.com/actions/runne"
},
{
"path": "images/macos/macos-14-arm64-Readme.md",
"chars": 23570,
"preview": "| Announcements |\n|-|\n| [macOS 26 (Tahoe) is now generally available in GitHub Actions](https://github.com/actions/runne"
},
{
"path": "images/macos/macos-15-Readme.md",
"chars": 22375,
"preview": "| Announcements |\n|-|\n| [macOS 26 (Tahoe) is now generally available in GitHub Actions](https://github.com/actions/runne"
},
{
"path": "images/macos/macos-15-arm64-Readme.md",
"chars": 24096,
"preview": "| Announcements |\n|-|\n| [macOS 26 (Tahoe) is now generally available in GitHub Actions](https://github.com/actions/runne"
},
{
"path": "images/macos/macos-26-Readme.md",
"chars": 14132,
"preview": "| Announcements |\n|-|\n| [macOS 26 (Tahoe) is now generally available in GitHub Actions](https://github.com/actions/runne"
},
{
"path": "images/macos/macos-26-arm64-Readme.md",
"chars": 14967,
"preview": "| Announcements |\n|-|\n| [macOS 26 (Tahoe) is now generally available in GitHub Actions](https://github.com/actions/runne"
},
{
"path": "images/macos/scripts/build/Configure-Toolset.ps1",
"chars": 1649,
"preview": "################################################################################\n## File: Configure-Toolset.ps1\n## Te"
},
{
"path": "images/macos/scripts/build/Configure-Xcode-Simulators.ps1",
"chars": 4027,
"preview": "################################################################################\n## File: Configure-Xcode-Simulators.p"
},
{
"path": "images/macos/scripts/build/Install-Toolset.ps1",
"chars": 2531,
"preview": "################################################################################\n## File: Install-Toolset.ps1\n## Team"
},
{
"path": "images/macos/scripts/build/Install-Xcode.ps1",
"chars": 2555,
"preview": "################################################################################\n## File: Install-Xcode.ps1\n## Desc: "
},
{
"path": "images/macos/scripts/build/Update-XcodeSimulators.ps1",
"chars": 2274,
"preview": "################################################################################\n## File: Update-XcodeSimulators.ps1\n#"
},
{
"path": "images/macos/scripts/build/configure-auto-updates.sh",
"chars": 566,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-autologin.sh",
"chars": 1867,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: "
},
{
"path": "images/macos/scripts/build/configure-hostname.sh",
"chars": 1249,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-machine.sh",
"chars": 3899,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-ntpconf.sh",
"chars": 622,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-preimagedata.sh",
"chars": 1525,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-shell.sh",
"chars": 660,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-ssh.sh",
"chars": 432,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-system.sh",
"chars": 4077,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-tccdb-macos.sh",
"chars": 12662,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-windows.sh",
"chars": 1244,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/configure-xcode.sh",
"chars": 1444,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: c"
},
{
"path": "images/macos/scripts/build/install-actions-cache.sh",
"chars": 905,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: "
},
{
"path": "images/macos/scripts/build/install-android-sdk.sh",
"chars": 5565,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-audiodevice.sh",
"chars": 432,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-aws-tools.sh",
"chars": 690,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-azcopy.sh",
"chars": 649,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-bicep.sh",
"chars": 380,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-chrome.sh",
"chars": 2528,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-cocoapods.sh",
"chars": 471,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-codeql-bundle.sh",
"chars": 2283,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-common-utils.sh",
"chars": 3719,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-dotnet.sh",
"chars": 1729,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-edge.sh",
"chars": 2530,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-firefox.sh",
"chars": 585,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-gcc.sh",
"chars": 622,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-git.sh",
"chars": 1266,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-homebrew.sh",
"chars": 1355,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-llvm.sh",
"chars": 378,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-mono.sh",
"chars": 2350,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-nginx.sh",
"chars": 407,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-node.sh",
"chars": 615,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-openjdk.sh",
"chars": 3701,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-openssl.sh",
"chars": 1179,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-php.sh",
"chars": 460,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-postgresql.sh",
"chars": 1171,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-powershell.sh",
"chars": 2630,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-python.sh",
"chars": 980,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-rosetta.sh",
"chars": 335,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-ruby.sh",
"chars": 2239,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-rubygems.sh",
"chars": 721,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-rust.sh",
"chars": 673,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-safari.sh",
"chars": 957,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-swiftlint.sh",
"chars": 375,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-unxip.sh",
"chars": 604,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-vcpkg.sh",
"chars": 746,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/build/install-xcode-clt.sh",
"chars": 1814,
"preview": "#!/bin/bash -e -o pipefail\n################################################################################\n## File: i"
},
{
"path": "images/macos/scripts/docs-gen/Generate-SoftwareReport.ps1",
"chars": 8615,
"preview": "using module ./software-report-base/SoftwareReport.psm1\nusing module ./software-report-base/SoftwareReport.Nodes.psm1\n\np"
},
{
"path": "images/macos/scripts/docs-gen/SoftwareReport.Android.psm1",
"chars": 7036,
"preview": "Import-Module \"$PSScriptRoot/SoftwareReport.Helpers.psm1\" -DisableNameChecking\nImport-Module \"$PSScriptRoot/../helpers/C"
},
{
"path": "images/macos/scripts/docs-gen/SoftwareReport.Browsers.psm1",
"chars": 3559,
"preview": "function Build-BrowserSection {\n\n $nodes = @()\n $os = Get-OSVersion\n\n $nodes += @(\n [ToolVersionNode]::n"
},
{
"path": "images/macos/scripts/docs-gen/SoftwareReport.Common.psm1",
"chars": 13105,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\nfunction Get-BashVersion {\n $version = bash -c 'echo ${"
},
{
"path": "images/macos/scripts/docs-gen/SoftwareReport.Helpers.psm1",
"chars": 1406,
"preview": "function Run-Command {\n param (\n [Parameter(Mandatory=$true)]\n [string] $Command,\n [switch] $Sup"
},
{
"path": "images/macos/scripts/docs-gen/SoftwareReport.Java.psm1",
"chars": 838,
"preview": "function Get-JavaVersions {\n $defaultJavaPath = (Get-Item env:JAVA_HOME).value\n\n $os = Get-OSVersion\n if ($os.I"
},
{
"path": "images/macos/scripts/docs-gen/SoftwareReport.Toolcache.psm1",
"chars": 1649,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\nfunction Get-ToolcacheRubyVersions {\n $toolcachePath = "
},
{
"path": "images/macos/scripts/docs-gen/SoftwareReport.Xcode.psm1",
"chars": 7779,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/../helpers/Xcode.Helpers.psm1\""
},
{
"path": "images/macos/scripts/helpers/Common.Helpers.psm1",
"chars": 4006,
"preview": "function Get-CommandResult {\n param (\n [Parameter(Mandatory=$true)]\n [string] $Command,\n [switch"
},
{
"path": "images/macos/scripts/helpers/Xcode.Helpers.psm1",
"chars": 4441,
"preview": "function Get-XcodeRootPath {\n Param (\n [Parameter(Mandatory)]\n [string] $Version\n )\n\n return \"/Ap"
},
{
"path": "images/macos/scripts/helpers/Xcode.Installer.psm1",
"chars": 11960,
"preview": "Import-Module \"$PSScriptRoot/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Xcode.Helpers.psm1\"\n\nfunction Install-Xco"
},
{
"path": "images/macos/scripts/helpers/confirm-identified-developers-macos14.scpt",
"chars": 938,
"preview": "# This AppleScript clicks \"Allow\" for \"System Software from developer \"Parallels International GmbH\"\n# Steps:\n# - Open S"
},
{
"path": "images/macos/scripts/helpers/confirm-identified-developers-macos15.scpt",
"chars": 864,
"preview": "# This AppleScript clicks \"Allow\" for \"System Software from developer \"Parallels International GmbH\"\n# Steps:\n# - Open S"
},
{
"path": "images/macos/scripts/helpers/invoke-tests.sh",
"chars": 205,
"preview": "#!/bin/bash -e -o pipefail\n\nsource $HOME/.bashrc\npwsh -Command \"Import-Module '$HOME/image-generation/tests/Helpers.psm1"
},
{
"path": "images/macos/scripts/helpers/utils.sh",
"chars": 5571,
"preview": "#!/bin/bash -e -o pipefail\n\ndownload_with_retry() {\n url=$1\n download_path=$2\n\n if [ -z \"$download_path\" ]; the"
},
{
"path": "images/macos/scripts/tests/ActionArchiveCache.Tests.ps1",
"chars": 644,
"preview": "Describe \"ActionArchiveCache\" {\n Context \"Action archive cache directory not empty\" {\n It \"$HOME/actionarchive"
},
{
"path": "images/macos/scripts/tests/Android.Tests.ps1",
"chars": 3364,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChec"
},
{
"path": "images/macos/scripts/tests/BasicTools.Tests.ps1",
"chars": 3381,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"Azure CLI\" {\n It \"Azure "
},
{
"path": "images/macos/scripts/tests/Browsers.Tests.ps1",
"chars": 2513,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"Chrome\" {\n BeforeAll {\n "
},
{
"path": "images/macos/scripts/tests/Common.Tests.ps1",
"chars": 2568,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\r\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChe"
},
{
"path": "images/macos/scripts/tests/Git.Tests.ps1",
"chars": 282,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"Git\" {\n It \"git is insta"
},
{
"path": "images/macos/scripts/tests/Helpers.psm1",
"chars": 4661,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\nfunction Confirm-ArrayWithoutDuplicates {\n param (\n "
},
{
"path": "images/macos/scripts/tests/Java.Tests.ps1",
"chars": 2967,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChec"
},
{
"path": "images/macos/scripts/tests/LLVM.Tests.ps1",
"chars": 402,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\nDescribe \"Clang/LLVM\" {\n BeforeAll {\n $toolsetVe"
},
{
"path": "images/macos/scripts/tests/Linters.Tests.ps1",
"chars": 215,
"preview": "Import-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChecking\n\n$os = Get-OSVersion\n\nDescribe \"SwiftLint\" -Skip:($os.Is"
},
{
"path": "images/macos/scripts/tests/Mono.Tests.ps1",
"chars": 2967,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChec"
},
{
"path": "images/macos/scripts/tests/Node.Tests.ps1",
"chars": 949,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChec"
},
{
"path": "images/macos/scripts/tests/OpenSSL.Tests.ps1",
"chars": 1167,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"OpenSSL\" {\n Context \"Ope"
},
{
"path": "images/macos/scripts/tests/PHP.Tests.ps1",
"chars": 681,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"PHP\" {\n Context \"PHP\" -S"
},
{
"path": "images/macos/scripts/tests/Powershell.Tests.ps1",
"chars": 1434,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChec"
},
{
"path": "images/macos/scripts/tests/Python.Tests.ps1",
"chars": 1227,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChec"
},
{
"path": "images/macos/scripts/tests/Rosetta.Tests.ps1",
"chars": 333,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"Rosetta\" -Skip:(-not $os.Is"
},
{
"path": "images/macos/scripts/tests/Ruby.Tests.ps1",
"chars": 880,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChec"
},
{
"path": "images/macos/scripts/tests/RubyGem.Tests.ps1",
"chars": 610,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"RubyGems\" {\n $gemTestCas"
},
{
"path": "images/macos/scripts/tests/RunAll-Tests.ps1",
"chars": 88,
"preview": "Import-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChecking\n\nInvoke-PesterTests \"*\"\n"
},
{
"path": "images/macos/scripts/tests/Rust.Tests.ps1",
"chars": 471,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"Rust\" {\n Context \"Rust\" "
},
{
"path": "images/macos/scripts/tests/System.Tests.ps1",
"chars": 1715,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\n\n$os = Get-OSVersion\n\nDescribe \"Disk free space\" {\n It \""
},
{
"path": "images/macos/scripts/tests/Toolcache.Tests.ps1",
"chars": 7506,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/Helpers.psm1\" -DisableNameChec"
},
{
"path": "images/macos/scripts/tests/Toolset.Tests.ps1",
"chars": 908,
"preview": "Import-Module \"$PSScriptRoot/Helpers.psm1\"\n\n$toolsets = Get-ChildItem -Path $PSScriptRoot -Filter \"toolset-*.json\"\n\nfunc"
},
{
"path": "images/macos/scripts/tests/Xcode.Tests.ps1",
"chars": 5156,
"preview": "Import-Module \"$PSScriptRoot/../helpers/Common.Helpers.psm1\"\nImport-Module \"$PSScriptRoot/../helpers/Xcode.Helpers.psm1\""
},
{
"path": "images/macos/templates/macOS-14.anka.pkr.hcl",
"chars": 9604,
"preview": "packer {\n required_plugins {\n veertu-anka = {\n version = \">= v3.2.0\"\n source = \"github.com/veertuinc/veer"
},
{
"path": "images/macos/templates/macOS-14.arm64.anka.pkr.hcl",
"chars": 9414,
"preview": "packer {\n required_plugins {\n veertu-anka = {\n version = \">= v3.2.0\"\n source = \"github.com/veertuinc/veer"
},
{
"path": "images/macos/templates/macOS-15.anka.pkr.hcl",
"chars": 9543,
"preview": "packer {\n required_plugins {\n veertu-anka = {\n version = \">= v3.2.0\"\n source = \"github.com/veertuinc/veer"
},
{
"path": "images/macos/templates/macOS-15.arm64.anka.pkr.hcl",
"chars": 9358,
"preview": "packer {\n required_plugins {\n veertu-anka = {\n version = \">= v3.2.0\"\n source = \"github.com/veertuinc/veer"
},
{
"path": "images/macos/templates/macOS-26.anka.pkr.hcl",
"chars": 9350,
"preview": "packer {\n required_plugins {\n veertu-anka = {\n version = \">= v3.2.0\"\n source = \"github.com/veertuinc/veer"
},
{
"path": "images/macos/templates/macOS-26.arm64.anka.pkr.hcl",
"chars": 9300,
"preview": "packer {\n required_plugins {\n veertu-anka = {\n version = \">= v3.2.0\"\n source = \"github.com/veertuinc/veer"
},
{
"path": "images/macos/toolsets/Readme.md",
"chars": 3507,
"preview": "# Toolset JSON structure\n\n## Xcode\n\n- `versions` - the array of objects that will present installed Xcode versions \n -"
},
{
"path": "images/macos/toolsets/toolset-14.json",
"chars": 10658,
"preview": "{\n \"xcode\": {\n \"default\": \"15.4\",\n \"x64\": {\n \"versions\": [\n {\n "
},
{
"path": "images/macos/toolsets/toolset-15.json",
"chars": 12551,
"preview": "{\n \"xcode\": {\n \"default\": \"16.4\",\n \"x64\": {\n \"versions\": [\n {\n "
},
{
"path": "images/macos/toolsets/toolset-26.json",
"chars": 9258,
"preview": "{\n \"xcode\": {\n \"default\": \"26.2\",\n \"x64\": {\n \"versions\": [\n {\n "
},
{
"path": "images/ubuntu/Ubuntu2204-Readme.md",
"chars": 16302,
"preview": "| Announcements |\n|-|\n| [[Windows/Ubuntu] Docker Server and Client will be updated to version 29.1.*, Docker Compose wil"
},
{
"path": "images/ubuntu/Ubuntu2404-Readme.md",
"chars": 13991,
"preview": "| Announcements |\n|-|\n| [[Windows/Ubuntu] Docker Server and Client will be updated to version 29.1.*, Docker Compose wil"
},
{
"path": "images/ubuntu/assets/post-gen/cleanup-logs.sh",
"chars": 313,
"preview": "#!/bin/bash\n\n# journalctl\nif command -v journalctl; then\n journalctl --rotate\n journalctl --vacuum-time=1s\nfi\n\n# d"
},
{
"path": "images/ubuntu/assets/post-gen/environment-variables.sh",
"chars": 230,
"preview": "#!/bin/bash\n\n# Replace $HOME with the default user's home directory for environmental variables related to the default u"
},
{
"path": "images/ubuntu/assets/post-gen/systemd-linger.sh",
"chars": 132,
"preview": "#!/bin/bash\n\n# Enable user session on boot, not on login\nUserId=$(cut -d: -f3 /etc/passwd | tail -1)\nloginctl enable-lin"
},
{
"path": "images/ubuntu/assets/ubuntu2204.conf",
"chars": 63,
"preview": "# Name of pool supported by this image\nPOOL_NAME=\"Ubuntu 2204\"\n"
},
{
"path": "images/ubuntu/scripts/build/Configure-Toolset.ps1",
"chars": 3162,
"preview": "################################################################################\n## File: Configure-Toolset.ps1\n## Te"
},
{
"path": "images/ubuntu/scripts/build/Install-PowerShellAzModules.ps1",
"chars": 1252,
"preview": "################################################################################\n## File: Install-PowerShellAzModules."
},
{
"path": "images/ubuntu/scripts/build/Install-PowerShellModules.ps1",
"chars": 1237,
"preview": "################################################################################\n## File: Install-PowerShellModules.ps"
},
{
"path": "images/ubuntu/scripts/build/Install-Toolset.ps1",
"chars": 1994,
"preview": "################################################################################\n## File: Install-Toolset.ps1\n## Team"
},
{
"path": "images/ubuntu/scripts/build/cleanup.sh",
"chars": 1151,
"preview": "#!/bin/bash -e\n################################################################################\n## File: cleanup.sh\n##"
},
{
"path": "images/ubuntu/scripts/build/configure-apt-mock.sh",
"chars": 2020,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-apt"
},
{
"path": "images/ubuntu/scripts/build/configure-apt-sources.sh",
"chars": 1182,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-apt"
},
{
"path": "images/ubuntu/scripts/build/configure-apt.sh",
"chars": 2100,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-apt"
},
{
"path": "images/ubuntu/scripts/build/configure-dpkg.sh",
"chars": 1843,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-dpk"
},
{
"path": "images/ubuntu/scripts/build/configure-environment.sh",
"chars": 3737,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-env"
},
{
"path": "images/ubuntu/scripts/build/configure-image-data.sh",
"chars": 1233,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-ima"
},
{
"path": "images/ubuntu/scripts/build/configure-limits.sh",
"chars": 811,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-lim"
},
{
"path": "images/ubuntu/scripts/build/configure-snap.sh",
"chars": 995,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-sna"
},
{
"path": "images/ubuntu/scripts/build/configure-system.sh",
"chars": 1464,
"preview": "#!/bin/bash -e\n################################################################################\n## File: configure-syst"
},
{
"path": "images/ubuntu/scripts/build/install-actions-cache.sh",
"chars": 1239,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-"
},
{
"path": "images/ubuntu/scripts/build/install-aliyun-cli.sh",
"chars": 1015,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-aliyu"
},
{
"path": "images/ubuntu/scripts/build/install-android-sdk.sh",
"chars": 5472,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-andro"
},
{
"path": "images/ubuntu/scripts/build/install-apache.sh",
"chars": 461,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-apach"
},
{
"path": "images/ubuntu/scripts/build/install-apt-common.sh",
"chars": 627,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-apt-c"
},
{
"path": "images/ubuntu/scripts/build/install-apt-vital.sh",
"chars": 452,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-apt-v"
},
{
"path": "images/ubuntu/scripts/build/install-aws-tools.sh",
"chars": 1517,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-aws-t"
},
{
"path": "images/ubuntu/scripts/build/install-azcopy.sh",
"chars": 642,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-azcop"
},
{
"path": "images/ubuntu/scripts/build/install-azure-cli.sh",
"chars": 673,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-azure"
},
{
"path": "images/ubuntu/scripts/build/install-azure-devops-cli.sh",
"chars": 744,
"preview": "#!/bin/bash -e\n################################################################################\n## File: install-azure"
}
]
// ... and 304 more files (download for full content)
About this extraction
This page contains the full source code of the actions/runner-images GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 504 files (1.5 MB), approximately 433.8k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.