Full Code of actions/runner-images for AI

main 563ad669b646 cached
504 files
1.5 MB
433.8k tokens
1 symbols
1 requests
Download .txt
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>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fubuntu24.json) | x64 | `ubuntu-latest` or `ubuntu-24.04` | [ubuntu-24.04] |
| Ubuntu 22.04<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fubuntu22.json) | x64 | `ubuntu-22.04` | [ubuntu-22.04] |
| Ubuntu Slim ![preview](https://img.shields.io/badge/preview-0969DA?style=flat&logoColor=white)<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fubuntu-slim.json) | x64 | `ubuntu-slim` | [ubuntu-slim] |
| macOS 26 Arm64<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fmacos-26-arm64.json) | arm64 | `macos-26` or `macos-26-xlarge` | [macOS-26-arm64] |
| macOS 26<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fmacos-26.json) | x64 | `macos-26-intel`, `macos-26-large` | [macOS-26] |
| macOS 15<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fmacos-15.json) | x64 | `macos-latest-large`, `macos-15-large`, or `macos-15-intel` | [macOS-15] |
| macOS 15 Arm64<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fmacos-15-arm64.json) | arm64 | `macos-latest`, `macos-15`, or `macos-15-xlarge` | [macOS-15-arm64] |
| macOS 14<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fmacos-14.json) | x64 | `macos-14-large`| [macOS-14] |
| macOS 14 Arm64<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fmacos-14-arm64.json) | arm64 | `macos-14` or `macos-14-xlarge`| [macOS-14-arm64] |
| Windows Server 2025 with Visual Studio 2026 ![beta](https://img.shields.io/badge/beta-yellow)<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fwin25-vs2026.json) | x64 | `windows-2025-vs2026` | [windows-2025-vs2026] |
| Windows Server 2025<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fwin25.json) | x64 | `windows-latest` or `windows-2025` | [windows-2025] |
| Windows Server 2022<br>![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fhosted-runners-images-bot%2F79267492faab096d04cdd25ce7014cec%2Fraw%2Fwin22.json) | 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
Download .txt
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
Download .txt
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.

Copied to clipboard!