Compare commits

..

1 Commits

Author SHA1 Message Date
Image generation service account a519e52ca6 Updating readme file for macOS-12 version 20220220.1 2022-02-21 10:41:07 +00:00
749 changed files with 25737 additions and 32712 deletions
+1 -1
View File
@@ -1 +1 @@
* @actions/runner-images-team
* @actions/virtual-environments-owners
+9 -18
View File
@@ -28,30 +28,21 @@ body:
required: true
- type: checkboxes
attributes:
label: Platforms affected
label: Virtual environments 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 13
- label: macOS 13 Arm64
- label: macOS 14
- label: macOS 14 Arm64
- label: macOS 15
- label: macOS 15 Arm64
- label: macOS 26 Arm64
- label: Ubuntu 18.04
- label: Ubuntu 20.04
- label: macOS 10.15
- label: macOS 11
- label: Windows Server 2016
- label: Windows Server 2019
- label: Windows Server 2022
- label: Windows Server 2025
validations:
required: true
- type: textarea
attributes:
label: Mitigation ways
description: Steps or options for impact mitigation
validations:
required: true
+10 -27
View File
@@ -10,35 +10,24 @@ body:
required: true
- type: checkboxes
attributes:
label: Platforms affected
label: Virtual environments 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 13
- label: macOS 13 Arm64
- label: macOS 14
- label: macOS 14 Arm64
- label: macOS 15
- label: macOS 15 Arm64
- label: macOS 26 Arm64
- label: Ubuntu 18.04
- label: Ubuntu 20.04
- label: macOS 10.15
- label: macOS 11
- label: Windows Server 2016
- label: Windows Server 2019
- label: Windows Server 2022
- label: Windows Server 2025
validations:
required: true
- 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".
1. For GitHub Actions, under "Set up job" -> "Virtual Environment" -> "Version".
2. For Azure DevOps, under "Initialize job" -> "Virtual Environment" -> "Version".
If you have a public example, please, provide a link to the failed build.
validations:
@@ -47,20 +36,14 @@ body:
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
+2 -2
View File
@@ -2,5 +2,5 @@ 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!
url: https://github.com/actions/virtual-environments/discussions
about: Have a question? Feel free to ask in the virtual-environments GitHub Discussions!
+10 -20
View File
@@ -44,32 +44,22 @@ body:
label: URL for tool's homepage
- type: textarea
attributes:
label: Provide a basic test case to validate the tool's functionality.
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
label: Virtual environments affected
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 13
- label: macOS 13 Arm64
- label: macOS 14
- label: macOS 14 Arm64
- label: macOS 15
- label: macOS 15 Arm64
- label: macOS 26 Arm64
- label: Ubuntu 18.04
- label: Ubuntu 20.04
- label: macOS 10.15
- label: macOS 11
- label: Windows Server 2016
- label: Windows Server 2019
- label: Windows Server 2022
- label: Windows Server 2025
validations:
required: true
- type: textarea
attributes:
label: Can this tool be installed during the build?
@@ -82,4 +72,4 @@ body:
- type: input
attributes:
label: Are you willing to submit a PR?
description: We accept contributions!
description: We accept contributions!
-39
View File
@@ -1,39 +0,0 @@
# 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.
+2 -2
View File
@@ -1,6 +1,6 @@
# 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.
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 -->
@@ -1,22 +0,0 @@
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-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Validate JSON Schema
shell: pwsh
run: ./helpers/CheckOutdatedVersionPinning.ps1
env:
GH_TOKEN: ${{ github.token }}
-72
View File
@@ -1,72 +0,0 @@
# 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
+8 -5
View File
@@ -10,12 +10,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create release for ${{ github.event.client_payload.ReleaseBranchName }}
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b #v1.20.0
uses: actions/create-release@v1.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ github.event.client_payload.ReleaseBranchName }}
name: ${{ github.event.client_payload.ReleaseTitle }}
tag_name: ${{ github.event.client_payload.ReleaseBranchName }}
release_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
commitish: ${{ github.event.client_payload.Commitish }}
+12 -26
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
fetch-depth: 0
@@ -18,46 +18,32 @@ jobs:
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
git push origin ${{ github.event.client_payload.ReleaseBranchName }}-docs
- name: Create pull request for ${{ github.event.client_payload.ReleaseBranchName }}
id: create-pr
uses: actions/github-script@v8
uses: actions/github-script@v2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pulls = await github.rest.pulls.list({
let response = await github.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:${{ github.event.client_payload.ReleaseBranchName }}-docs`,
title: "${{ github.event.client_payload.PullRequestTitle }}",
head: "${{ github.event.client_payload.ReleaseBranchName }}-docs",
base: "${{ github.event.client_payload.PullRequestBase }}",
state: 'open'
body: `${{ github.event.client_payload.PullRequestBody }}`
});
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;
}
return response.data.number
- name: Request reviewers
uses: actions/github-script@v8
uses: actions/github-script@v2
with:
github-token: ${{secrets.PRAPPROVAL_SECRET}}
script: |
github.rest.pulls.requestReviewers({
github.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ steps.create-pr.outputs.result }},
team_reviewers: ['runner-images-team']
})
team_reviewers: ['virtual-environments-akvelon']
})
-112
View File
@@ -1,112 +0,0 @@
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
+2 -2
View File
@@ -17,12 +17,12 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v5
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Lint Code Base
uses: github/super-linter/slim@v7
uses: github/super-linter/slim@v4
env:
VALIDATE_ALL_CODEBASE: false
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+16 -5
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
fetch-depth: 0
@@ -24,11 +24,11 @@ jobs:
sleep 30
- name: Approve pull request by GitHub-Actions bot
uses: actions/github-script@v8
uses: actions/github-script@v2
with:
github-token: ${{secrets.PRAPPROVAL_SECRET}}
script: |
github.rest.pulls.createReview({
github.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.client_payload.PullRequestNumber }},
@@ -36,13 +36,24 @@ jobs:
});
- name: Merge pull request for ${{ github.event.client_payload.ReleaseBranchName }}
uses: actions/github-script@v8
uses: actions/github-script@v2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.pulls.merge({
github.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.client_payload.PullRequestNumber }},
merge_method: "squash"
})
- name: Delete docs branch ${{ github.event.client_payload.ReleaseBranchName }}-docs
uses: actions/github-script@v2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: "heads/${{ github.event.client_payload.ReleaseBranchName }}-docs"
})
-25
View File
@@ -1,25 +0,0 @@
# 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"
@@ -1,119 +0,0 @@
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 }}")
-20
View File
@@ -1,20 +0,0 @@
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
-20
View File
@@ -1,20 +0,0 @@
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
+5 -3
View File
@@ -10,17 +10,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Update release for ${{ github.event.client_payload.ReleaseBranchName }}
uses: actions/github-script@v8
uses: actions/github-script@v2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const response = await github.rest.repos.getReleaseByTag({
const response = await github.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: "${{ github.event.client_payload.ReleaseBranchName }}"
});
github.rest.repos.updateRelease({
github.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: response.data.id,
@@ -1,20 +0,0 @@
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
-20
View File
@@ -1,20 +0,0 @@
name: Trigger Windows19 CI
run-name: Windows2019 - ${{ github.event.pull_request.title }}
on:
pull_request_target:
types: labeled
paths:
- 'images/windows/**'
defaults:
run:
shell: pwsh
jobs:
Windows_2019:
if: github.event.label.name == 'CI windows-all' || github.event.label.name == 'CI windows-2019'
uses: ./.github/workflows/trigger-ubuntu-win-build.yml
with:
image_type: 'windows2019'
secrets: inherit
-20
View File
@@ -1,20 +0,0 @@
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
-20
View File
@@ -1,20 +0,0 @@
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
+1 -10
View File
@@ -267,12 +267,6 @@ paket-files/
.idea/
*.sln.iml
# VSCode settings
.vscode/**
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/tasks.json
# CodeRush
.cr/
@@ -393,7 +387,4 @@ public
.dynamodb/
# visual studio code launch configuration
launch.json
# Ignore dynamic template
images/*/*-temp.json
launch.json
-9
View File
@@ -1,9 +0,0 @@
{
"recommendations": [
"streetsidesoftware.code-spell-checker",
"hashicorp.hcl",
"davidanson.vscode-markdownlint",
"ms-vscode.powershell",
"timonwong.shellcheck"
]
}
+1 -32
View File
@@ -1,34 +1,3 @@
{
"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"
}
]
"files.trimTrailingWhitespace": false
}
+33 -224
View File
@@ -1,250 +1,59 @@
# Contributing
## Contributing
[fork]: https://github.com/actions/runner-images/fork
[pr]: https://github.com//actions/runner-images/compare
[fork]: https://github.com/actions/virtual-environments/fork
[pr]: https://github.com//actions/virtual-environments/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)
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).
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.
## 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].
1. [Fork][fork] and clone the repository
1. Create a new branch: `git checkout -b my-branch-name`
1. Make your changes, ensure that they include steps to install, validate post-install and update software report (please see [How to add new tool](CONTRIBUTING.md#how-to-add-new-tool) for details).
1. Test your changes by [creating VHD 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.
- 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).
- Write [good commit messages](http://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
- Make sure that the tool satisfies [Software Guidelines](README.md#software-guidelines).
- Create an issue and get an approval from us to add this tool to the image before creating the pull request.
## How to add new tool
### 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-2019.json](images/windows/toolsets/toolset-2019.json) as an example.
- Use consistent naming across all files.
- Validation scripts should be simple and shouldn't change the image content.
- For every new tool add validation scripts and update software report script to make sure that it is included to documentation
- If the tool is available in other platforms (macOS, Windows, Linux), make sure you include it in as many as possible.
- If installing a few versions of the tool, consider putting the list of versions in the corresponding `toolset.json` file. It will help other customers to configure their builds flexibly. See [toolset-windows-2016.json](images/win/toolsets/toolset-2019.json) as example.
- Use consistent naming across all files
- Validation scripts should be simple and shouldn't change 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-IsWin19`, `Test-IsWin22` (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. [Windows2019-Readme.md](images/windows/Windows2019-Readme.md) and uses [MarkdownPS](https://github.com/Sarafian/MarkdownPS).
- Add a script that will install the tool and put the script in the `scripts/Installers` folder.
There are a bunch of helper functions that could simplify your code: `Choco-Install`, `Install-Binary`, `Install-VsixExtension`, `Start-DownloadWithRetry`, `Test-IsWin16`, `Test-IsWin19` (find the full list of helpers in [ImageHelpers.psm1](images/win/scripts/ImageHelpers/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 make sure that your tests will be run.
- Add changes to the software report generator `images/win/scripts/SoftwareReport/SoftwareReport.Generator.ps1`. The software report generator is used to generate an image's README file, e.g. [Windows2019-Readme.md](images/win/Windows2019-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).
- Add script that will install and validate the tool and put the script in the `scripts/installers` folder.
Use existing scripts such as [github-cli.sh](images/linux/scripts/installers/github-cli.sh) as a starting point.
- Use [helpers](images/linux/scripts/helpers/install.sh) to simplify installation process.
- Validation part should `exit 1` if any issue with installation.
- Add changes to the software report generator `images/linux/scripts/SoftwareReport/SoftwareReport.Generator.ps1`. The software report generator is used to generate an image's README file, e.g. [Ubuntu1804-Readme.md](images/linux/Ubuntu1804-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.
macOS source lives in this repository and 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 continue to make tool requests by filing issues.
## Resources
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 GitHub
Copyright (c) 2021 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
+59 -217
View File
@@ -1,217 +1,59 @@
# 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 | YAML Label | Included Software |
| --------------------|---------------------|--------------------|
| Ubuntu 24.04 | `ubuntu-latest` or `ubuntu-24.04` | [ubuntu-24.04] |
| Ubuntu 22.04 | `ubuntu-22.04` | [ubuntu-22.04] |
| macOS 26 Arm64 `beta` | `macos-26` or `macos-26-xlarge` | [macOS-26-arm64] |
| macOS 15 | `macos-latest-large`, `macos-15-large`, or `macos-15-intel` | [macOS-15] |
| macOS 15 Arm64 | `macos-latest`, `macos-15`, or `macos-15-xlarge` | [macOS-15-arm64] |
| macOS 14 | `macos-14-large`| [macOS-14] |
| macOS 14 Arm64 | `macos-14` or `macos-14-xlarge`| [macOS-14-arm64] |
| macOS 13 ![Deprecated](https://img.shields.io/badge/-Deprecated-red) | `macos-13` or `macos-13-large` | [macOS-13] |
| macOS 13 Arm64 ![Deprecated](https://img.shields.io/badge/-Deprecated-red) | `macos-13-xlarge` | [macOS-13-arm64] |
| Windows Server 2025 | `windows-latest` or `windows-2025` | [windows-2025] |
| Windows Server 2022 | `windows-2022` | [windows-2022] |
| Windows Server 2019 ![Deprecated](https://img.shields.io/badge/-Deprecated-red) | `windows-2019` | [windows-2019] |
### 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
[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
[windows-2019]: https://github.com/actions/runner-images/blob/main/images/windows/Windows2019-Readme.md
[windows-2025]: https://github.com/actions/runner-images/blob/main/images/windows/Windows2025-Readme.md
[windows-2022]: https://github.com/actions/runner-images/blob/main/images/windows/Windows2022-Readme.md
[macOS-13]: https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md
[macOS-13-arm64]: https://github.com/actions/runner-images/blob/main/images/macos/macos-13-arm64-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-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 [twitter](https://twitter.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 topic in a [discussion](https://github.com/actions/runner-images/discussions) for a corresponding category. Before making a new discussion please make sure no similar topics were created earlier.
- For general questions about using the runner images or writing your Actions workflow, please open requests in the [GitHub Actions Community Forum](https://github.community/c/github-actions/41).
## 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>
# GitHub Actions Virtual Environments
This repository contains the source used to create the [virtual environments](https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners) for GitHub Actions hosted runners, as well as the VM images of [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).
How to get in touch with us:
- To file a bug report, or request tools to be added/updated, please [open an issue using the appropriate template](https://github.com/actions/virtual-environments/issues/new/choose)
- If you want to share your thoughts about image configuration, installed software, or bring some idea, please, create a new topic in a [discussions section](https://github.com/actions/virtual-environments/discussions) for a corresponding category. Before making a new discussion please make sure no similar topics were created earlier.
For general questions about using the virtual environments or writing your Actions workflow, please open requests in the [GitHub Actions Community Forum](https://github.community/c/github-actions/41).
## Available Environments
| Environment | YAML Label | Included Software | Latest Release & Rollout Progress |
| --------------------|---------------------|--------------------|---------------------|
| Ubuntu 20.04 | `ubuntu-latest` or `ubuntu-20.04` | [ubuntu-20.04] | [![](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=ubuntu20&badge=1)](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=ubuntu20&redirect=1)
| Ubuntu 18.04 | `ubuntu-18.04` | [ubuntu-18.04] | [![](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=ubuntu18&badge=1)](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=ubuntu18&redirect=1)
| macOS 11 | `macos-latest` or `macos-11`| [macOS-11] | [![](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=macos-11&badge=1)](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=macos-11&redirect=1)
| macOS 10.15 | `macos-10.15` | [macOS-10.15] | [![](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=macos-10.15&badge=1)](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=macos-10.15&redirect=1)
| Windows Server 2022 | `windows-2022` | [windows-2022] | [![](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=windows-2022&badge=1)](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=windows-2022&redirect=1) |
| Windows Server 2019 | `windows-latest` or `windows-2019` | [windows-2019] | [![](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=windows-2019&badge=1)](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=windows-2019&redirect=1)
| Windows Server 2016 | `windows-2016` | [windows-2016] | [![](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=windows-2016&badge=1)](https://actionvirtualenvironmentsstatus.azurewebsites.net/api/status?imageName=windows-2016&redirect=1)
<b>Note:</b> Beta and Preview images are provided "as-is", "with all faults" and "as available" and are excluded from the service level agreement and warranty. Beta and Preview images may not be covered by customer support.
***What images are available for GitHub Actions and Azure DevOps?***
The availability of images for GitHub Actions and Azure DevOps is different. 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)
***What image version is used in my build?*** Usually, image deployment takes 3-4 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.
***Looking for other Linux distributions?*** 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 virtual environments. Alternatively, you can leverage [self-hosted runners] and fully customize your environment to your needs.
***How to contribute to macOS source?*** macOS source lives in this repository and 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 continue to make tool requests by filing issues.
## Updates to virtual environments
*Cadence*
We typically deploy weekly updates to the software on the virtual environments.
For some tools, we always install the latest at the time of the deployment; for others,
we pin the tool to specific version(s).
*Following Along / Change Notifications*
* **High Impact Changes** (ex. breaking changes, new or deprecated environments) will be posted to the GitHub Changelog on our [blog](https://github.blog/changelog/) and on [twitter](https://twitter.com/GHchangelog).
* **Low Impact Changes** will be pinned in this repository and marked with the [Announcement](https://github.com/actions/virtual-environments/labels/Announcement) label.
* **Regular Weekly Rhythm** can be followed by watching [Releases](https://github.com/actions/virtual-environments/releases). Pre-release is created when deployment is started. As soon as deployment is finished, it is converted to release.
You can also track upcoming changes using the [awaiting-deployment](https://github.com/actions/virtual-environments/labels/awaiting-deployment) label.
[ubuntu-20.04]: https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md
[ubuntu-18.04]: https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu1804-Readme.md
[windows-2022]: https://github.com/actions/virtual-environments/blob/main/images/win/Windows2022-Readme.md
[windows-2019]: https://github.com/actions/virtual-environments/blob/main/images/win/Windows2019-Readme.md
[windows-2016]: https://github.com/actions/virtual-environments/blob/main/images/win/Windows2016-Readme.md
[macOS-11]: https://github.com/actions/virtual-environments/blob/main/images/macos/macos-11-Readme.md
[macOS-10.15]: https://github.com/actions/virtual-environments/blob/main/images/macos/macos-10.15-Readme.md
[self-hosted runners]: https://help.github.com/en/actions/hosting-your-own-runners
## Software and image guidelines
To learn more about tools and images support policy, see the [guidelines](./docs/software-and-images-guidelines.md).
-294
View File
@@ -1,294 +0,0 @@
#!/usr/bin/env bash
################################################################################
## File: diff-image-versions.sh
## Desc: Compare software versions between two runner image releases
## Usage: ./diff-image-versions.sh <os-name> <version1> <version2>
##
## Example:
## ./diff-image-versions.sh ubuntu22 20251102.127 20251125.163
## ./diff-image-versions.sh win25 20251102.77 20251125.122
## ./diff-image-versions.sh macos-14 20251102.0024 20251125.0031
################################################################################
set -euo pipefail
usage() {
cat <<EOF
Usage: $(basename "${0}") <os-name> <version1> <version2>
Compare runner image versions and display software changes.
Arguments:
os-name OS identifier (ubuntu22, ubuntu24, win19, win22, win25,
macos-13, macos-14, macos-15, or arm64 variants)
version1 Earlier version (YYYYMMDD.NNN)
version2 Later version (YYYYMMDD.NNN)
Examples:
$(basename "${0}") ubuntu22 20251102.127 20251125.163
$(basename "${0}") win25 20251102.77 20251125.122
EOF
}
get_readme_path() {
local os_name="${1}"
local os_folder=""
local pattern=""
# Determine OS folder and readme filename pattern
case "${os_name}" in
ubuntu*)
os_folder="ubuntu"
local version="${os_name#ubuntu}"
pattern="Ubuntu${version}04-Readme.md"
;;
win*)
os_folder="windows"
local version="${os_name#win}"
pattern="Windows20${version}-Readme.md"
;;
macos*)
os_folder="macos"
pattern="${os_name}-Readme.md"
;;
*)
echo "Error: Unknown OS '${os_name}'" >&2
echo "Valid: ubuntu*, win*, macos-*" >&2
return 1
;;
esac
local readme_path="images/${os_folder}/${pattern}"
# Verify file exists in git repository
if ! git cat-file -e "HEAD:${readme_path}" 2>/dev/null; then
echo "Error: Readme not found: ${readme_path}" >&2
return 1
fi
echo "${readme_path}"
}
validate_version() {
local version="${1}"
if [[ ! "${version}" =~ ^[0-9]{8}\.[0-9]+$ ]]; then
echo "Error: Invalid version '${version}'" >&2
echo "Format: YYYYMMDD.NNN (e.g., 20251102.127)" >&2
return 1
fi
return 0
}
tag_exists() {
local tag="${1}"
if git rev-parse "${tag}" >/dev/null 2>&1; then
return 0
else
echo "Error: Tag '${tag}' not found" >&2
return 1
fi
}
main() {
# Check arguments
if [[ $# -ne 3 ]]; then
usage
return 1
fi
local os_name="${1}"
local version1="${2}"
local version2="${3}"
# Validate inputs
validate_version "${version1}" || return 1
validate_version "${version2}" || return 1
# Get readme path
local readme_path
readme_path="$(get_readme_path "${os_name}")" || return 1
# Construct git tags
local tag1="${os_name}/${version1}"
local tag2="${os_name}/${version2}"
# Verify tags exist
tag_exists "${tag1}" || return 1
tag_exists "${tag2}" || return 1
# Get release dates
local date1
local date2
date1=$(git log -1 --format="%ci" "${tag1}" | cut -d' ' -f1)
date2=$(git log -1 --format="%ci" "${tag2}" | cut -d' ' -f1)
# Calculate days between releases
local days_diff
days_diff=$(( ($(date -d "${date2}" +%s) - $(date -d "${date1}" +%s)) / 86400 ))
# Display header
echo "================================================================================"
echo "Comparing: ${os_name}"
echo " From: ${version1} (${date1})"
echo " To: ${version2} (${date2})"
echo " Span: ${days_diff} days"
echo "================================================================================"
echo ""
# Perform diff with minimal context (only changed lines with colors)
# ANSI codes: ^[[31m (red for -), ^[[32m (green for +), ^[[36m (cyan for @@)
# Filter to show only lines starting with red/green (additions/deletions)
local diff_output
diff_output=$(git diff --color=always --unified=0 "${tag1}:${readme_path}" "${tag2}:${readme_path}" | \
grep -E $'^\x1b\\[(31|32)m' | \
grep -v -E $'^\x1b\\[1m(---|\\+\\+\\+)')
if [[ -n "${diff_output}" ]]; then
# Extract announcements from both versions
local announcements1
local announcements2
announcements1=$(git show "${tag1}:${readme_path}" | sed -n '/| Announcements |/,/^\*\*\*$/p' | grep -E '^\| \[' | sed 's/^| \[/• [/' | sed 's/ |$//' || true)
announcements2=$(git show "${tag2}:${readme_path}" | sed -n '/| Announcements |/,/^\*\*\*$/p' | grep -E '^\| \[' | sed 's/^| \[/• [/' | sed 's/ |$//' || true)
# Show announcement changes
if [[ "${announcements1}" != "${announcements2}" ]]; then
echo "📢 Announcement Changes:"
echo "────────────────────────────────────────────────────────────────────────────────"
if [[ -n "${announcements2}" ]]; then
echo "${announcements2}"
else
echo "(no announcements)"
fi
echo "────────────────────────────────────────────────────────────────────────────────"
echo ""
fi
# Extract cached tools sections
local cached_tools1
local cached_tools2
cached_tools1=$(git show "${tag1}:${readme_path}" | sed -n '/^### Cached Tools$/,/^###[^#]/p' | head -n -1 || true)
cached_tools2=$(git show "${tag2}:${readme_path}" | sed -n '/^### Cached Tools$/,/^###[^#]/p' | head -n -1 || true)
# Show cached tools changes
if [[ "${cached_tools1}" != "${cached_tools2}" ]]; then
local cached_diff
cached_diff=$(git diff --color=always --unified=2 --no-index \
<(echo "${cached_tools1}") <(echo "${cached_tools2}") 2>/dev/null | \
grep -E $'(^\x1b\\[(31|32)m[-+]| #### )' | \
sed -r 's/\x1b\[m$//' || true)
if [[ -n "${cached_diff}" ]]; then
echo "🔧 Cached Tools Changes (setup-* actions):"
echo "────────────────────────────────────────────────────────────────────────────────"
echo "${cached_diff}"
echo "────────────────────────────────────────────────────────────────────────────────"
echo ""
fi
fi
echo "Full Diff:"
echo "────────────────────────────────────────────────────────────────────────────────"
echo "${diff_output}"
echo "────────────────────────────────────────────────────────────────────────────────"
echo ""
# Count changes
local changes
changes=$(echo "${diff_output}" | wc -l)
echo "Changes: ${changes} lines"
# Parse version changes for breaking change analysis
local breaking_changes=()
local removals=()
local additions=()
# Extract clean lines (strip ANSI codes)
while IFS= read -r line; do
if [[ "${line}" =~ ^\-(.+)$ ]]; then
removals+=("${BASH_REMATCH[1]}")
elif [[ "${line}" =~ ^\+(.+)$ ]]; then
additions+=("${BASH_REMATCH[1]}")
fi
done < <(echo "${diff_output}" | sed -r 's/\x1b\[[0-9;]*m//g')
# Detect breaking changes
for removed in "${removals[@]}"; do
local tool_name=""
local old_version=""
local found_match=false
# Try to extract tool name and version (handle various formats)
if [[ "${removed}" =~ ^([^0-9]+[[:space:]]+)([0-9]+\.[0-9]+[^[:space:]]*) ]]; then
tool_name="${BASH_REMATCH[1]}"
old_version="${BASH_REMATCH[2]}"
elif [[ "${removed}" =~ ^([^0-9]+[[:space:]]+v)([0-9]+\.[0-9]+[^[:space:]]*) ]]; then
tool_name="${BASH_REMATCH[1]}"
old_version="${BASH_REMATCH[2]}"
fi
# If we found a semver-style version, look for matching addition
if [[ -n "${tool_name}" && -n "${old_version}" ]]; then
for added in "${additions[@]}"; do
if [[ "${added}" =~ ^${tool_name}([0-9]+\.[0-9]+[^[:space:]]*) ]]; then
local new_version="${BASH_REMATCH[1]}"
found_match=true
# Extract major version for semver comparison
if [[ "${old_version}" =~ ^([0-9]+)\. && "${new_version}" =~ ^([0-9]+)\. ]]; then
local old_major="${BASH_REMATCH[1]}"
local new_major="${BASH_REMATCH[1]}"
[[ "${old_version}" =~ ^([0-9]+)\. ]] && old_major="${BASH_REMATCH[1]}"
[[ "${new_version}" =~ ^([0-9]+)\. ]] && new_major="${BASH_REMATCH[1]}"
if [[ ${new_major} -gt ${old_major} ]]; then
breaking_changes+=("🔴 ${tool_name}${old_version}${new_version} (major version bump)")
fi
fi
break
fi
done
fi
# If no match found and looks like a versioned tool, it's a removal
if [[ ${found_match} == false && -n "${old_version}" ]]; then
breaking_changes+=("${removed} (removed)")
elif [[ ${found_match} == false && "${removed}" =~ [0-9]+\.[0-9]+ ]]; then
breaking_changes+=("${removed} (removed)")
fi
done
# Display breaking changes
if [[ ${#breaking_changes[@]} -gt 0 ]]; then
echo ""
echo "⚠️ Breaking changes detected (${#breaking_changes[@]}):"
echo "--------------------------------------------------------------------------------"
printf '%s\n' "${breaking_changes[@]}"
echo "--------------------------------------------------------------------------------"
fi
else
echo "No changes found."
fi
# Display PR link and commit count
local pr_number
pr_number=$(git log --all --format="%s" --grep="${version2}" | \
grep -oP '\(#\K[0-9]+(?=\))' | head -1)
local commit_count
commit_count=$(git rev-list --count "${tag1}..${tag2}")
echo "Commits: ${commit_count}"
if [[ -n "${pr_number}" ]]; then
echo "PR: https://github.com/actions/runner-images/pull/${pr_number}"
fi
return 0
}
# Execute main function
main "$@"
+128 -268
View File
@@ -1,317 +1,177 @@
# GitHub Actions Runner Images
# Virtual-Environments
The virtual-environments project uses [Packer](https://www.packer.io/) to generate disk images for the following platforms: Windows 2016/2019/2022, Ubuntu 18.04/20.04.
Each image is configured through a JSON template that Packer understands and which specifies where to build the image (Azure in this case), and what scripts to run to install software and prepare the disk.
The Packer process initializes a connection to Azure subscription via Azure CLI, and automatically creates the temporary Azure resources required to build the source VM(temporary resource group, network interfaces, and VM from the "clean" image specified in the template).
If the VM deployment succeeds, the build agent connects to the VM and starts to execute installation steps from the JSON template.
If any step in the JSON template fails, image generation will be aborted and the temporary VM will be terminated. Packer will also attempt to cleanup all the temporary resources it created (unless otherwise told).
After successful image generation, a snapshot of the temporary VM will be converted to VHD image and then uploaded to the specified Azure Storage Account.
The runner-images project uses [Packer](https://www.packer.io/) to generate disk images for Windows 2019/2022 and Ubuntu 22.04/24.04.
## Prerequisites and Image-generation
### Build Agent requirements
- `OS` - Windows/Linux
- `packer` - Can be downloaded from https://www.packer.io/downloads
- `PowerShell 5.0 or higher` or `PSCore` for linux distributes.
- `Azure CLI ` - https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
- `Azure Az Powershell module` - https://docs.microsoft.com/en-us/powershell/azure/install-az-ps
- `Git for Windows` - https://gitforwindows.org/
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.
> To connect to a temporary VM packer uses WinRM or SSH connections on public IP interfaces.
If you use a build agent located in an Azure subscription, please make sure that HTTPS/SSH ports are allowed for incoming/outgoing connections.
In case of firewall restrictions, prohibiting connections from public addresses, private virtual network resources can be deployed and passed as arguments to the packer. This approach allows virtual machines to use private connections inside VLAN.
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.
### Service principal
Packer uses Service Principal to authorize in Azure infrastructure. To setup image-generation CI or use packer manually — SP with full read-write permissions for selected Azure subscription needed.
Detailed instruction can be found in [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal)
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).
### Prepare environment and image deployment
#### How to prepare Windows build agent
Local machine or [Azure VM](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/quick-create-cli) can be used as a build agent.
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
Download `packer` from https://www.packer.io/downloads, or install it via Chocolately.
```
choco install packer
```
Then, import the [GenerateResourcesAndImage](../helpers/GenerateResourcesAndImage.ps1) script from the `helpers` subdirectory:
Install the Azure Az PowerShell module - https://docs.microsoft.com/en-us/powershell/azure/install-az-ps.
```
Install-Module -Name Az -Repository PSGallery -Force
```
Install Azure CLI - https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest&tabs=azure-cli.
```
Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi; Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'; rm .\AzureCLI.msi
```
Download Virtual-Environments repository.
```
Set-Location c:\
git clone https://github.com/actions/virtual-environments.git
```
Import [GenerateResourcesAndImage](../helpers/GenerateResourcesAndImage.ps1) script from `/helpers` folder, and run `GenerateResourcesAndImage` function via Powershell.
```
Set-Location C:\virtual-environments
```powershell
Import-Module .\helpers\GenerateResourcesAndImage.ps1
GenerateResourcesAndImage -SubscriptionId {YourSubscriptionId} -ResourceGroupName "myTestResourceGroup" -ImageGenerationRepositoryRoot "$pwd" -ImageType Ubuntu1804 -AzureLocation "East US"
```
Where:
- `SubscriptionId` - The Azure subscription Id where resources will be created.
- `ResourceGroupName` - The Azure resource group name where the Azure resources will be created.
- `ImageGenerationRepositoryRoot` - The root path of the image generation repository source.
- `ImageType` - The type of the image being generated. Valid options are: "Windows2016", "Windows2019", "Windows2022", "Ubuntu1804", "Ubuntu2004".
- `AzureLocation` - The location of the resources being created in Azure. For example "East US".
The function automatically creates all required Azure resources and kicks off packer image generation for the selected image type.
For optional authentication via service principal make sure to provide the following params — `AzureClientId`, `AzureClientSecret`, `AzureTenantId`, so the whole command will be:
```
GenerateResourcesAndImage -SubscriptionId {YourSubscriptionId} -ResourceGroupName "myTestResourceGroup" -ImageGenerationRepositoryRoot "$pwd" -ImageType Ubuntu1804 -AzureLocation "East US" -AzureClientId {AADApplicationID} -AzureClientSecret {AADApplicationSecret} -AzureTenantId {AADTenantID}
```
Finally, run the `GenerateResourcesAndImage` function, setting the mandatory arguments: image type and where to build and store the resulting managed image:
*Please, check synopsis of `GenerateResourcesAndImage` for details about non-mandatory parameters.*
- `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 "Windows2019", "Windows2022", "Windows2025", "Ubuntu2204", "Ubuntu2404").
#### Generated VM Deployment
After the successful image generation, Virtual Machine can be created from the generated VHD using [CreateAzureVMFromPackerTemplate](../helpers/CreateAzureVMFromPackerTemplate.ps1) script.
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
}
```
Set-Location C:\virtual-environments
## 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"
CreateAzureVMFromPackerTemplate -SubscriptionId {YourSubscriptionId} -ResourceGroupName {ResourceGroupName} -TemplateFile "C:\BuildVmImages\temporaryTemplate.json" -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.
- `TemplateFilePath` - The path to the json ARM-template generated by packer during image generation locally.*
- `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. Example: "eastus"
- `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").
\* *ARM-template can be obtained from the Packer output. For now, it seems like there is an [Az CLI bug](https://github.com/Azure/azure-cli/issues/5899) with specifying the template through a URI, so download the template from URI, that will be printed at the bottom of image-generation log, and use the local path of the template file.*
This function creates an Azure VM and generates network resources in Azure to make the VM accessible.
The function creates an Azure VM from a template and generates network resources in Azure to make the VM accessible.
## Automated image generation
## Additional
### User variables
The Packer template includes `variables` section containing user variables used in image generation. Each variable is defined as a key/value strings. User variables can be passed to packer via predefined environment variables, or as direct arguments, in case if packer started manually.
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
- `build_resource_group_name` - Specify an existing resource group to run the build in it. 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 it.
- `client_id` - The application ID of the AAD Service Principal. Requires `client_secret`.
- `object_id` - The object ID for the AAD SP. Will be derived from the oAuth token if empty.
- `client_secret` - A password/secret registered for the AAD SP.
- `subscription_id` - The subscription to use.
- `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`.
- `resource_group` - Resource group under which the final artifact will be stored.
- `storage_account` - Storage account under which the final artifact will be stored.
- `location` - Azure datacenter in which your VM will be built.
- `temp_resource_group_name` - 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.
- `capture_name_prefix` - VHD prefix. The final artifacts will be named PREFIX-osDisk.UUID and PREFIX-vmTemplate.UUID.
### 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.
- `vm_size` - Size of the VM used for building. This can be changed when you deploy a VM from your VHD.
- `image_os` - Type of OS that will be deployed as a temporary VM.
- `image_version` - Specify 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).**
**Detailed Azure builders documentation can be found in [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.
### Toolset
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 to 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
### Post-generation scripts
> :warning: These scripts are intended to be run on a VM deployed in Azure
> :warning: These scripts are intended to 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>
The user, created during the image generation, does not exist in the result VHD 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-generation` folder in the repository:
- Windows: https://github.com/actions/virtual-environments/tree/main/images/win/post-generation
- Linux: https://github.com/actions/virtual-environments/tree/main/images/linux/post-generation
**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:
The scripts are copied to the VHD during the image generation process to the following paths:
- Windows: `C:\post-generation`
- Linux: `/opt/post-generation`
- Linux: `/opt/post-generation`
### Running scripts
#### Running scripts
- Ubuntu
##### Ubuntu
```bash
sudo su -c "find /opt/post-generation -mindepth 1 -maxdepth 1 -type f -name '*.sh' -exec bash {} \;"
```
sudo su -c "find /opt/post-generation -mindepth 1 -maxdepth 1 -type f -name '*.sh' -exec bash {} \;"
- Windows
##### Windows
```powershell
Get-ChildItem C:\post-generation -Filter *.ps1 | ForEach-Object { & $_.FullName }
```
Get-ChildItem C:\post-generation -Filter *.ps1 | ForEach-Object { & $_.FullName }
### Script Details: Ubuntu
#### Script details
- **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).
##### Ubuntu
### Script Details: Windows
- **cleanup-logs.sh** - removes all build process logs from the machine
- **environment-variables.sh** - replaces `$HOME` with the default user's home directory for environmental variables related to the default user home directory
- **homebrew-permissions.sh** - Resets homebrew repository directory by running `git reset --hard` to make the working tree clean after chmoding /home and changes the repository directory owner to the current user
- **rust-permissions.sh** - fixes permissions for the Rust folder. Detailed issue explanation is provided in [virtual-environments/issues/572](https://github.com/actions/virtual-environments/issues/572).
- **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.
##### Windows
- **Choco.ps1** - contains dummy command to cleanup orphaned packages to avoid initial delay for future choco commands
- **Dotnet.ps1** - adds `$env:USERPROFILE\.dotnet\tools` directory to the PATH
- **InternetExplorerConfiguration** - turns off the Internet Explorer Enhanced Security feature
- **Msys2FirstLaunch.ps1** - initializes bash user profile in MSYS2
- **RustJunction.ps1** - creates Rust junction points to cargo and rustup folders
- **VSConfiguration.ps1** - performs initial Visual Studio configuration
+27
View File
@@ -0,0 +1,27 @@
# Debugging Failed Packer Builds
## Step 1: Run packer build `-on-error=ask`
When you run the `packer build` command, give it the `-on-error=ask` flag.
By default, `packer build` will delete the resource group as soon as the build fails.
`-on-error=ask` will pause it and wait for your input so you have time to remote in to the VM and diagnose the failure.
When the build fails, you will see this:
![Ask on error screenshot](/docs/resources/askOnError.png "Ask on error screenshot")
## Step 2: Find the resource group name in the build log
At the beginning of the build log (written to console), find the resource group name for the VM:
![Resource group from log screenshot](/docs/resources/resourceGroupName.png "Resource group from log screenshot")
Log into the Azure Portal. Find that resource group under `Resource groups`. You should see the resources for the Packer build:
![Packer resource group in Azure screenshot](/docs/resources/packerResourceGroup.png "Packer resource group in Azure screenshot")
## Step 3: Connect to the VM
Select the VM in the resource group. Click `Connect:`
This will download an RDP file. Open that and enter the credentials found in the JSON file you pass to `packer build`:
![VM credentials screenshot](/docs/resources/vmCredentials.png "VM credentials screenshot")
-34
View File
@@ -1,34 +0,0 @@
# 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 2004 and 2204
2004 and 2204 use the [Microsoft Package repository](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu-install?tabs=dotnet8&pivots=os-linux-ubuntu-2004) 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

+38
View File
@@ -0,0 +1,38 @@
# Software and image guidelines
## Software preinstallation policy
In general, these are the guidelines we consider when deciding what to pre-install:
- Tools and ecosystems that are broadly popular and widely-used will be given priority.
- Recent versions of tools will be given priority over older versions.
- Tools and versions that are deprecated or have reached end-of-life will not be added.
- If a tool can be installed during the build, we will evaluate how much time is saved
and how much space is used by having the tool pre-installed.
- MIT, Apache, and GNU licenses are ok, anything else we'll have to check with lawyers.
- If a tool takes much space we will evaluate space usage and provide a decision if this tool can be pre-installed.
- If a tool requires the support of more than one version, we will consider the cost of this maintenance, how often new versions bring dangerous updates.
**Note:** For new tools, please, create an issue and get an approval from us to add this tool to the image before creating the pull request.
## Software and images support policy
These are the guidelines we follow in software and images supporting routine:
- Tools and versions will typically be removed 6 months after they are deprecated or have reached end-of-life.
- We support at least 2 latest OS versions (LTS only for Ubuntu) and initiate deprecation process for the oldest one when image usage drops below 5%.
- The images generally contain the latest versions of packages installed except for Ubuntu LTS where we rely on the Canonical-provided repositories mostly.
- 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 |
| GCC <br/> GNU Fortran <br/> Clang <br/> GNU C++ | 3 latest major versions |
| Android NDK | 1 latest non-LTS, 2 latest LTS versions |
| Xcode | - all OS compatible versions side-by-side <br/> - for beta, GM versions - latest beta only <br/> - old patch versions are deprecated in 3 months |
## Software default versions update policy for tools with multiple versions installed
In general, once a new version is installed on the image, we announce the default version update 2 weeks prior to deploying it to give time to adapt to upcoming changes. For potentially dangerous updates, we can extend the timeline up to 1 month between the announcement and deployment.
-42
View File
@@ -1,42 +0,0 @@
$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"
}
-85
View File
@@ -1,85 +0,0 @@
$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
}
}
+8 -17
View File
@@ -12,8 +12,8 @@ Function CreateAzureVMFromPackerTemplate {
.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 TemplatFilePath
The path for the json template generated by packer during image generation locally.
.PARAMETER VirtualMachineName
The name of the virtual machine to be generated.
@@ -28,7 +28,7 @@ Function CreateAzureVMFromPackerTemplate {
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"
CreateAzureVMFromPackerTemplate -SubscriptionId {YourSubscriptionId} -ResourceGroupName {ResourceGroupName} -TemplateFile "C:\BuildVmImages\temporaryTemplate.json" -VirtualMachineName "testvm1" -AdminUsername "shady1" -AdminPassword "SomeSecurePassword1" -AzureLocation "eastus"
#>
param (
[Parameter(Mandatory = $True)]
@@ -36,7 +36,7 @@ Function CreateAzureVMFromPackerTemplate {
[Parameter(Mandatory = $True)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $True)]
[string] $ManagedImageName,
[string] $TemplateFilePath,
[Parameter(Mandatory = $True)]
[string] $VirtualMachineName,
[Parameter(Mandatory = $True)]
@@ -52,7 +52,7 @@ Function CreateAzureVMFromPackerTemplate {
$vnetName = $env:UserName + "vnet-" + $guid
$subnetName = $env:UserName + "subnet-" + $guid
$nicName = $env:UserName + "nic-" + $guid
$publicIpName = $env:UserName + "pip-" + $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)
@@ -63,23 +63,14 @@ Function CreateAzureVMFromPackerTemplate {
$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)
($publicIp = az network public-ip create -g $ResourceGroupName -l $AzureLocation -n $publicIpName --allocation-method Static --sku Standard --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
az group deployment create -g $ResourceGroupName -n $VirtualMachineName --subscription $subscriptionId --template-file $templateFilePath --parameters vmSize=$vmSize vmName=$VirtualMachineName adminUserName=$AdminUsername adminPassword=$AdminPassword networkInterfaceId=$networkId
Write-Host "`nCreated in ${ResourceGroupName}:`n vnet ${vnetName}`n subnet ${subnetName}`n nic ${nicName}`n publicip ${publicIpName}`n vm ${VirtualMachineName}"
}
+189 -242
View File
@@ -1,14 +1,14 @@
$ErrorActionPreference = 'Stop'
enum ImageType {
Windows2019 = 1
Windows2022 = 2
Windows2025 = 3
Ubuntu2204 = 4
Ubuntu2404 = 5
Windows2016 = 0
Windows2019 = 1
Windows2022 = 2
Ubuntu1804 = 3
Ubuntu2004 = 4
}
Function Get-PackerTemplate {
Function Get-PackerTemplatePath {
param (
[Parameter(Mandatory = $True)]
[string] $RepositoryRoot,
@@ -17,109 +17,87 @@ Function Get-PackerTemplate {
)
switch ($ImageType) {
# Note: Double Join-Path is required to support PowerShell 5.1
([ImageType]::Windows2016) {
$relativeTemplatePath = Join-Path "win" "windows2016.json"
}
([ImageType]::Windows2019) {
$relativeTemplatePath = Join-Path (Join-Path "windows" "templates") "build.windows-2019.pkr.hcl"
$imageOS = "win19"
$relativeTemplatePath = Join-Path "win" "windows2019.json"
}
([ImageType]::Windows2022) {
$relativeTemplatePath = Join-Path (Join-Path "windows" "templates") "build.windows-2022.pkr.hcl"
$imageOS = "win22"
$relativeTemplatePath = Join-Path "win" "windows2022.json"
}
([ImageType]::Windows2025) {
$relativeTemplatePath = Join-Path (Join-Path "windows" "templates") "build.windows-2025.pkr.hcl"
$imageOS = "win25"
([ImageType]::Ubuntu1804) {
$relativeTemplatePath = Join-Path "linux" "ubuntu1804.json"
}
([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"
([ImageType]::Ubuntu2004) {
$relativeTemplatePath = Join-Path "linux" "ubuntu2004.json"
}
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'."
throw "Template for image '$ImageType' doesn't exist on path '$imageTemplatePath'"
}
return [PSCustomObject] @{
"BuildName" = $buildName
"ImageOS" = $imageOS
"Path" = [IO.Path]::GetDirectoryName($imageTemplatePath)
}
return $imageTemplatePath;
}
Function Show-LatestCommit {
Function Get-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."
Write-Host "Latest commit:"
git --no-pager log --pretty=format:"Date: %cd; Commit: %H - %s; Author: %an <%ae>" -1
}
}
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.
Creates Azure resources and kicks off a packer image generation for the selected image type.
.PARAMETER SubscriptionId
The Azure subscription id where the Azure resources will be created.
The Azure subscription Id where 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: Windows2019, Windows2022, Windows2025, 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"
The Azure resource group name where the Azure resources will be created.
.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.
The root path of the image generation repository source.
.PARAMETER ImageType
The type of the image being generated. Valid options are: {"Windows2016", "Windows2019", "Windows2022", "Ubuntu1804", "Ubuntu2004"}.
.PARAMETER AzureLocation
The location of the resources being created in Azure. For example "East US".
.PARAMETER Force
Delete the resource group if it exists without user confirmation.
.PARAMETER AzureClientId
The Azure client id to use to authenticate with Azure. If not specified, the current user's credentials will be used.
Client id needs to be provided for optional authentication via service principal. Example: "11111111-1111-1111-1111-111111111111"
.PARAMETER AzureClientSecret
The Azure client secret to use to authenticate with Azure. If not specified, the current user's credentials will be used.
Client secret needs to be provided for optional authentication via service principal. Example: "11111111-1111-1111-1111-111111111111"
.PARAMETER AzureTenantId
The Azure tenant id to use to authenticate with Azure. If not specified, the current user's credentials will be used.
Tenant needs to be provided for optional authentication via service principal. Example: "11111111-1111-1111-1111-111111111111"
.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".
.PARAMETER AllowBlobPublicAccess
The Azure storage account will be created with this option.
.EXAMPLE
GenerateResourcesAndImage -SubscriptionId {YourSubscriptionId} -ResourceGroupName "shsamytest1" -ImageGenerationRepositoryRoot "C:\runner-images" -ImageType Ubuntu2204 -AzureLocation "East US"
GenerateResourcesAndImage -SubscriptionId {YourSubscriptionId} -ResourceGroupName "shsamytest1" -ImageGenerationRepositoryRoot "C:\virtual-environments" -ImageType Ubuntu1804 -AzureLocation "East US"
#>
param (
[Parameter(Mandatory = $True)]
@@ -128,14 +106,12 @@ Function GenerateResourcesAndImage {
[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,
[int] $SecondsToWaitForServicePrincipalSetup = 30,
[Parameter(Mandatory = $False)]
[string] $AzureClientId,
[Parameter(Mandatory = $False)]
@@ -143,182 +119,153 @@ Function GenerateResourcesAndImage {
[Parameter(Mandatory = $False)]
[string] $AzureTenantId,
[Parameter(Mandatory = $False)]
[string] $PluginVersion = "2.2.1",
[Switch] $RestrictToAgentIpAddress,
[Parameter(Mandatory = $False)]
[switch] $RestrictToAgentIpAddress,
[Switch] $Force,
[Parameter(Mandatory = $False)]
[ValidateSet("abort", "ask", "cleanup", "run-cleanup-provisioner")]
[string] $OnError = "ask",
[bool] $AllowBlobPublicAccess = $False,
[Parameter(Mandatory = $False)]
[hashtable] $Tags = @{}
[bool] $EnableHttpsTrafficOnly = $False
)
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."
$builderScriptPath = Get-PackerTemplatePath -RepositoryRoot $ImageGenerationRepositoryRoot -ImageType $ImageType
$ServicePrincipalClientSecret = $env:UserName + [System.GUID]::NewGuid().ToString().ToUpper()
$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..."
& $PackerBinary validate `
"-only=$($PackerTemplate.BuildName)*" `
"-var=client_id=fake" `
"-var=client_secret=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."
if ([string]::IsNullOrEmpty($AzureClientId))
{
Connect-AzAccount
} else {
$AzSecureSecret = ConvertTo-SecureString $AzureClientSecret -AsPlainText -Force
$AzureAppCred = New-Object System.Management.Automation.PSCredential($AzureClientId, $AzSecureSecret)
Connect-AzAccount -ServicePrincipal -Credential $AzureAppCred -Tenant $AzureTenantId
}
Set-AzContext -SubscriptionId $SubscriptionId
$alreadyExists = $true;
try {
# Login to Azure subscription
if ([string]::IsNullOrEmpty($AzureClientId)) {
Write-Verbose "No AzureClientId was provided, will use interactive login."
az login --output none
}
else {
Write-Verbose "AzureClientId was provided, will use service principal login."
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 service principal
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 {
$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 "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."
Get-AzResourceGroup -Name $ResourceGroupName
Write-Verbose "Resource group was found, will delete and recreate it."
}
}
catch {
Write-Verbose "Resource group was not found, will create it."
$alreadyExists = $false;
}
if ($alreadyExists) {
if($Force -eq $true) {
# Cleanup the resource group if it already exitsted before
Remove-AzResourceGroup -Name $ResourceGroupName -Force
New-AzResourceGroup -Name $ResourceGroupName -Location $AzureLocation
} else {
$title = "Delete Resource Group"
$message = "The resource group you specified already exists. Do you want to clean it up?"
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", `
"Delete the resource group including all resources."
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", `
"Keep the resource group and continue."
$stop = New-Object System.Management.Automation.Host.ChoiceDescription "&Stop", `
"Stop the current action."
$options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no, $stop)
$result = $host.ui.PromptForChoice($title, $message, $options, 0)
switch ($result)
{
0 { Remove-AzResourceGroup -Name $ResourceGroupName -Force; New-AzResourceGroup -Name $ResourceGroupName -Location $AzureLocation }
1 { <# Do nothing #> }
2 { exit }
}
}
} else {
New-AzResourceGroup -Name $ResourceGroupName -Location $AzureLocation
}
# This script should follow the recommended naming conventions for azure resources
$storageAccountName = if($ResourceGroupName.EndsWith("-rg")) {
$ResourceGroupName.Substring(0, $ResourceGroupName.Length -3)
} else { $ResourceGroupName }
# Resource group names may contain special characters, that are not allowed in the storage account name
$storageAccountName = $storageAccountName.Replace("-", "").Replace("_", "").Replace("(", "").Replace(")", "").ToLower()
$storageAccountName += "001"
New-AzStorageAccount -ResourceGroupName $ResourceGroupName -AccountName $storageAccountName -Location $AzureLocation -SkuName "Standard_LRS" -AllowBlobPublicAccess $AllowBlobPublicAccess -EnableHttpsTrafficOnly $EnableHttpsTrafficOnly
if ([string]::IsNullOrEmpty($AzureClientId)) {
# Interactive authentication: A service principal is created during runtime.
$spDisplayName = [System.GUID]::NewGuid().ToString().ToUpper()
$startDate = Get-Date
$endDate = $startDate.AddYears(1)
if ('Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential' -as [type]) {
$credentials = [Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential]@{
StartDate = $startDate
EndDate = $endDate
Password = $ServicePrincipalClientSecret
}
$sp = New-AzADServicePrincipal -DisplayName $spDisplayName -PasswordCredential $credentials
$spClientId = $sp.ApplicationId
$azRoleParam = @{
RoleDefinitionName = "Contributor"
ServicePrincipalName = $spClientId
}
}
if ('Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphPasswordCredential' -as [type]) {
$credentials = [Microsoft.Azure.PowerShell.Cmdlets.Resources.MSGraph.Models.ApiV10.MicrosoftGraphPasswordCredential]@{
StartDateTime = $startDate
EndDateTime = $endDate
}
$sp = New-AzADServicePrincipal -DisplayName $spDisplayName
$appCred = New-AzADAppCredential -ApplicationId $sp.AppId -PasswordCredentials $credentials
$spClientId = $sp.AppId
$azRoleParam = @{
RoleDefinitionName = "Contributor"
PrincipalId = $sp.Id
}
$ServicePrincipalClientSecret = $appCred.SecretText
}
Start-Sleep -Seconds $SecondsToWaitForServicePrincipalSetup
New-AzRoleAssignment @azRoleParam
Start-Sleep -Seconds $SecondsToWaitForServicePrincipalSetup
$sub = Get-AzSubscription -SubscriptionId $SubscriptionId
$tenantId = $sub.TenantId
# "", "Note this variable-setting script for running Packer with these Azure resources in the future:", "==============================================================================================", "`$spClientId = `"$spClientId`"", "`$ServicePrincipalClientSecret = `"$ServicePrincipalClientSecret`"", "`$SubscriptionId = `"$SubscriptionId`"", "`$tenantId = `"$tenantId`"", "`$spObjectId = `"$spObjectId`"", "`$AzureLocation = `"$AzureLocation`"", "`$ResourceGroupName = `"$ResourceGroupName`"", "`$storageAccountName = `"$storageAccountName`"", "`$install_password = `"$install_password`"", ""
} else {
# Parametrized Authentication via given service principal: The service principal with the data provided via the command line
# is used for all authentication purposes.
$spClientId = $AzureClientId
$credentials = $AzureAppCred
$ServicePrincipalClientSecret = $AzureClientSecret
$tenantId = $AzureTenantId
}
Get-LatestCommit -ErrorAction SilentlyContinue
$packerBinary = Get-Command "packer"
if (-not ($packerBinary)) {
throw "'packer' binary is not found on PATH"
}
if($RestrictToAgentIpAddress -eq $true) {
$AgentIp = (Invoke-RestMethod http://ipinfo.io/json).ip
Write-Host "Restricting access to packer generated VM to agent IP Address: $AgentIp"
}
& $packerBinary build -on-error=ask `
-var "client_id=$($spClientId)" `
-var "client_secret=$($ServicePrincipalClientSecret)" `
-var "subscription_id=$($SubscriptionId)" `
-var "tenant_id=$($tenantId)" `
-var "location=$($AzureLocation)" `
-var "resource_group=$($ResourceGroupName)" `
-var "storage_account=$($storageAccountName)" `
-var "install_password=$($InstallPassword)" `
-var "allowed_inbound_ip_addresses=$($AgentIp)" `
$builderScriptPath
}
-97
View File
@@ -1,97 +0,0 @@
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)
}
-47
View File
@@ -1,47 +0,0 @@
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
}
@@ -1,66 +0,0 @@
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
@@ -1,56 +0,0 @@
############################
### 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())
}
}
@@ -1,136 +0,0 @@
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
}
}
@@ -1,225 +0,0 @@
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]
}
}
@@ -1,439 +0,0 @@
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)
}
}
@@ -1,34 +0,0 @@
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"
}
}
@@ -1,558 +0,0 @@
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?
'@
}
}
@@ -1,603 +0,0 @@
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")
$prevReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddHeader("MySubheader2").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 1
$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", "MySubheader2")
$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 "Complex structure" {
$prevReport = [HeaderNode]::new("Version 1")
$prevSubHeader = $prevReport.AddHeader("MyHeader").AddHeader("MySubheader")
$prevSubHeader.AddToolVersion("MyTool1", "2.1.3")
$prevSubHeader.AddHeader("MySubSubheader").AddToolVersion("MyTool2", "2.9.1")
$prevReport.AddHeader("MyHeader2")
$prevReport.AddHeader("MyHeader3").AddHeader("MySubheader3").AddToolVersion("MyTool3", "14.2.1")
$nextReport = [HeaderNode]::new("Version 2")
$nextSubHeader = $nextReport.AddHeader("MyHeader").AddHeader("MySubheader")
$nextSubHeader.AddToolVersion("MyTool1", "2.1.4")
$nextSubSubHeader = $nextSubHeader.AddHeader("MySubSubheader")
$nextSubSubHeader.AddToolVersion("MyTool2", "2.9.1")
$nextSubSubHeader.AddToolVersion("MyTool4", "2.7.6")
$nextReport.AddHeader("MyHeader2")
$nextReport.AddHeader("MyHeader3")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 1
$comparer.ChangedItems | Should -HaveCount 1
$comparer.DeletedItems | Should -HaveCount 1
$comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty
$comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode])
$comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool4"
$comparer.AddedItems[0].CurrentReportNode.Version | Should -Be "2.7.6"
$comparer.AddedItems[0].Headers | Should -BeArray @("MyHeader", "MySubheader", "MySubSubheader")
$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")
$comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode])
$comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool3"
$comparer.DeletedItems[0].PreviousReportNode.Version | Should -Be "14.2.1"
$comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty
$comparer.DeletedItems[0].Headers | Should -BeArray @("MyHeader3", "MySubheader3")
}
}
Describe "ToolVersionNode" {
It "ToolVersionNode is updated" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersion("MyTool1", "2.1.3")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").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")
}
}
Describe "ToolVersionsListNode" {
It "Single version is not changed" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3"), "^.+")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersionsList("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 "Single version is changed" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3"), "^\d+")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.4"), "^\d+")
$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 ([ToolVersionsListNode])
$comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.1.3")
$comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].CurrentReportNode.Versions | Should -BeArray @("2.1.4")
}
It "Major version is added" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3"), "^\d+")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3", "3.1.4"), "^\d+")
$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 ([ToolVersionsListNode])
$comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.AddedItems[0].CurrentReportNode.Versions | Should -BeArray @("3.1.4")
}
It "Major version is removed" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3", "3.1.4"), "^\d+")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("3.1.4"), "^\d+")
$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 ([ToolVersionsListNode])
$comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.DeletedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.1.3")
$comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty
}
It "Major version is changed" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("3.1.4"), "^\d+")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("3.2.0"), "^\d+")
$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 ([ToolVersionsListNode])
$comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].PreviousReportNode.Versions | Should -BeArray @("3.1.4")
$comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].CurrentReportNode.Versions | Should -BeArray @("3.2.0")
}
It "Major version is added, removed and updated at the same time" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("1.0.0", "2.1.3", "3.1.4", "4.0.2"), "^\d+")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3", "3.2.0", "4.0.2", "5.1.0"), "^\d+")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 1
$comparer.ChangedItems | Should -HaveCount 1
$comparer.DeletedItems | Should -HaveCount 1
$comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty
$comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.AddedItems[0].CurrentReportNode.Versions | Should -BeArray @("5.1.0")
$comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].PreviousReportNode.Versions | Should -BeArray @("3.1.4")
$comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].CurrentReportNode.Versions | Should -BeArray @("3.2.0")
$comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.DeletedItems[0].PreviousReportNode.Versions | Should -BeArray @("1.0.0")
$comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty
}
It "Minor version is added, removed and updated at the same time" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.3.8", "2.4.9", "2.5.3", "2.6.0", "2.7.4", "2.8.0"), "^\d+\.\d+")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.5.3", "2.6.2", "2.7.5", "2.8.0", "2.9.2", "2.10.3"), "^\d+\.\d+")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 1
$comparer.ChangedItems | Should -HaveCount 1
$comparer.DeletedItems | Should -HaveCount 1
$comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty
$comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.AddedItems[0].CurrentReportNode.Versions | Should -BeArray @("2.9.2", "2.10.3")
$comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.6.0", "2.7.4")
$comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.ChangedItems[0].CurrentReportNode.Versions | Should -BeArray @("2.6.2", "2.7.5")
$comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.DeletedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.3.8", "2.4.9")
$comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty
}
It "Patch version is added, removed and updated at the same time" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.3.8", "2.4.9", "2.5.3", "2.6.0", "2.7.4"), "^\d+\.\d+\.\d+")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.4.9", "2.5.4", "2.6.0", "2.7.5", "2.8.2"), "^\d+\.\d+\.\d+")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 1
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 1
$comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty
$comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1"
$comparer.AddedItems[0].CurrentReportNode.Versions | Should -BeArray @("2.5.4", "2.7.5", "2.8.2")
$comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode])
$comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1"
$comparer.DeletedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.3.8", "2.5.3", "2.7.4")
$comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty
}
}
Describe "TableNode" {
It "Rows are added" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2")))
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2", "C1|C2", "D1|D2")))
$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 -BeOfType ([TableNode])
$comparer.AddedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value"
$comparer.AddedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2")
$comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([TableNode])
$comparer.AddedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value"
$comparer.AddedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2", "C1|C2", "D1|D2")
}
It "Rows are deleted" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2", "C1|C2", "D1|D2")))
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("C1|C2", "D1|D2")))
$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 ([TableNode])
$comparer.DeletedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value"
$comparer.DeletedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2", "C1|C2", "D1|D2")
$comparer.DeletedItems[0].CurrentReportNode | Should -BeOfType ([TableNode])
$comparer.DeletedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value"
$comparer.DeletedItems[0].CurrentReportNode.Rows | Should -BeArray @("C1|C2", "D1|D2")
}
It "Rows are changed" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2")))
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B3|B4")))
$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 ([TableNode])
$comparer.ChangedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value"
$comparer.ChangedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2")
$comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([TableNode])
$comparer.ChangedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value"
$comparer.ChangedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B3|B4")
}
It "Rows are changed and updated at the same time" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2")))
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B3|B4", "C1|C2")))
$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 ([TableNode])
$comparer.ChangedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value"
$comparer.ChangedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2")
$comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([TableNode])
$comparer.ChangedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value"
$comparer.ChangedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B3|B4", "C1|C2")
}
It "Rows are changed and removed at the same time" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2", "C1|C2")))
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B3|B4")))
$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 ([TableNode])
$comparer.ChangedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value"
$comparer.ChangedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2", "C1|C2")
$comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([TableNode])
$comparer.ChangedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value"
$comparer.ChangedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B3|B4")
}
It "Rows are not changed" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2")))
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2")))
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 0
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 0
}
It "Rows are not changed but header is changed" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2")))
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value2", @("A1|A2", "B1|B2")))
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 0
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 0
}
It "Rows are changed and header is changed at the same time" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2")))
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value2", @("A1|A2", "B1|B2", "C1|C2")))
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 1
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 1
$comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty
$comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([TableNode])
$comparer.AddedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value2"
$comparer.AddedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2", "C1|C2")
$comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([TableNode])
$comparer.DeletedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value"
$comparer.DeletedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2")
$comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty
}
}
Describe "NoteNode" {
It "NoteNode is ignored from report" {
$prevReport = [HeaderNode]::new("Version 1")
$prevReport.AddNote("MyFirstNote")
$prevReport.AddHeader("MyFirstHeader").AddNote("MyFirstSubNote")
$nextReport = [HeaderNode]::new("Version 2")
$nextReport.AddNote("MySecondNote")
$nextReport.AddHeader("MySecondHeader").AddNote("MySecondSubNote")
$comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport)
$comparer.CompareReports()
$comparer.AddedItems | Should -HaveCount 0
$comparer.ChangedItems | Should -HaveCount 0
$comparer.DeletedItems | Should -HaveCount 0
}
}
}
@@ -1,291 +0,0 @@
using module ../SoftwareReport.Nodes.psm1
using module ../SoftwareReport.DifferenceRender.psm1
BeforeDiscovery {
Import-Module $(Join-Path $PSScriptRoot "TestHelpers.psm1") -DisableNameChecking
}
Describe "ComparerReport.UnitTests" {
BeforeAll {
$script:DifferenceRender = [SoftwareReportDifferenceRender]::new()
}
Context "CalculateHtmlTableRowSpan" {
It "Without the equal cells" {
$table = @(
[PSCustomObject]@{ Key = "A"; Value = "1" }
[PSCustomObject]@{ Key = "B"; Value = "2" }
[PSCustomObject]@{ Key = "C"; Value = "3" }
)
$actual = $DifferenceRender.CalculateHtmlTableRowSpan($table, "Key")
$actual | Should -BeArray @(1, 1, 1)
}
It "Only equal cells" {
$table = @(
[PSCustomObject]@{ Key = "A"; Value = "D" }
[PSCustomObject]@{ Key = "B"; Value = "D" }
[PSCustomObject]@{ Key = "C"; Value = "D" }
)
$actual = $DifferenceRender.CalculateHtmlTableRowSpan($table, "Value")
$actual | Should -BeArray @(3, 0, 0)
}
It "Single row" {
$table = @(
[PSCustomObject]@{ Key = "A"; Value = "1" }
)
$actual = $DifferenceRender.CalculateHtmlTableRowSpan($table, "Key")
$actual | Should -BeArray @(1)
}
It "Different cells" {
$table = @(
[PSCustomObject]@{ Key = "A"; Value = "1" }
[PSCustomObject]@{ Key = "B"; Value = "2" }
[PSCustomObject]@{ Key = "B"; Value = "3" }
[PSCustomObject]@{ Key = "C"; Value = "4" }
[PSCustomObject]@{ Key = "C"; Value = "5" }
[PSCustomObject]@{ Key = "C"; Value = "6" }
[PSCustomObject]@{ Key = "D"; Value = "7" }
[PSCustomObject]@{ Key = "E"; Value = "8" }
[PSCustomObject]@{ Key = "E"; Value = "9" }
[PSCustomObject]@{ Key = "F"; Value = "10" }
)
$actual = $DifferenceRender.CalculateHtmlTableRowSpan($table, "Key")
$actual | Should -BeArray @(1, 2, 0, 3, 0, 0, 1, 2, 0, 1)
}
}
Context "RenderCategory" {
It "With line separator" {
$actual = $DifferenceRender.RenderCategory(@("Header 1", "Header 2", "Header 3"), $true)
$actual | Should -Be "Header 2 ><br> Header 3"
}
It "Without line separator" {
$actual = $DifferenceRender.RenderCategory(@("Header 1", "Header 2", "Header 3"), $false)
$actual | Should -Be "Header 2 > Header 3"
}
It "One header" {
$actual = $DifferenceRender.RenderCategory(@("Header 1"), $false)
$actual | Should -Be ""
}
It "Empty headers" {
$actual = $DifferenceRender.RenderCategory(@(), $false)
$actual | Should -Be ""
}
}
Context "RenderToolName" {
It "Clear tool name" {
$actual = $DifferenceRender.RenderToolName("My Tool 1")
$actual | Should -Be "My Tool 1"
}
It "Name with colon symbol" {
$actual = $DifferenceRender.RenderToolName("My Tool 1:")
$actual | Should -Be "My Tool 1"
}
}
Context "StrikeTableRow" {
It "Simple row" {
$actual = $DifferenceRender.StrikeTableRow("Test1|Test2|Test3")
$actual | Should -Be "~~Test1~~|~~Test2~~|~~Test3~~"
}
It "Row with spaces" {
$actual = $DifferenceRender.StrikeTableRow("Test 1|Test 2|Test 3")
$actual | Should -Be "~~Test 1~~|~~Test 2~~|~~Test 3~~"
}
}
Context "RenderHtmlTable" {
It "Simple table" {
$table = @(
[PSCustomObject]@{ "Category" = "A"; "Tool name" = "My Tool 1"; "Version" = "1.0" },
[PSCustomObject]@{ "Category" = "B"; "Tool name" = "My Tool 2"; "Version" = "2.0" },
[PSCustomObject]@{ "Category" = "C"; "Tool name" = "My Tool 3"; "Version" = "3.0" }
)
$renderedTable = $DifferenceRender.RenderHtmlTable($table, "Category")
$renderedTable | Should -Be @'
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Version</th>
</thead>
<tbody>
<tr>
<td rowspan="1">A</td>
<td>My Tool 1</td>
<td>1.0</td>
</tr>
<tr>
<td rowspan="1">B</td>
<td>My Tool 2</td>
<td>2.0</td>
</tr>
<tr>
<td rowspan="1">C</td>
<td>My Tool 3</td>
<td>3.0</td>
</tr>
</tbody>
</table>
'@
}
It "Table with the same category" {
$table = @(
[PSCustomObject]@{ "Category" = "A"; "Tool name" = "My Tool 1"; "Version" = "1.0" },
[PSCustomObject]@{ "Category" = "A"; "Tool name" = "My Tool 2"; "Version" = "2.0" },
[PSCustomObject]@{ "Category" = "A"; "Tool name" = "My Tool 3"; "Version" = "3.0" },
[PSCustomObject]@{ "Category" = "B"; "Tool name" = "My Tool 4"; "Version" = "4.0" }
)
$renderedTable = $DifferenceRender.RenderHtmlTable($table, "Category")
$renderedTable | Should -Be @'
<table>
<thead>
<th>Category</th>
<th>Tool name</th>
<th>Version</th>
</thead>
<tbody>
<tr>
<td rowspan="3">A</td>
<td>My Tool 1</td>
<td>1.0</td>
</tr>
<tr>
<td>My Tool 2</td>
<td>2.0</td>
</tr>
<tr>
<td>My Tool 3</td>
<td>3.0</td>
</tr>
<tr>
<td rowspan="1">B</td>
<td>My Tool 4</td>
<td>4.0</td>
</tr>
</tbody>
</table>
'@
}
}
Context "RenderTableNodesDiff" {
It "Add new table" {
$previousNode = $null
$currentNode = [TableNode]::new("Name|Value", @("A|1", "B|2"))
$reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3"))
$actual = $DifferenceRender.RenderTableNodesDiff($reportItem)
$actual | Should -Be @'
#### Header 2 > Header 3
| Name | Value |
| ---- | ----- |
| A | 1 |
| B | 2 |
'@
}
It "Remove existing table" {
$previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2"))
$currentNode = $null
$reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3"))
$actual = $DifferenceRender.RenderTableNodesDiff($reportItem)
$actual | Should -Be @'
#### Header 2 > Header 3
| Name | Value |
| ----- | ----- |
| ~~A~~ | ~~1~~ |
| ~~B~~ | ~~2~~ |
'@
}
It "Add new rows to existing table" {
$previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2"))
$currentNode = [TableNode]::new("Name|Value", @("A|1", "B|2", "C|3", "D|4"))
$reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3"))
$actual = $DifferenceRender.RenderTableNodesDiff($reportItem)
$actual | Should -Be @'
#### Header 2 > Header 3
| Name | Value |
| ---- | ----- |
| C | 3 |
| D | 4 |
'@
}
It "Remove rows from existing table" {
$previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2", "C|3", "D|4"))
$currentNode = [TableNode]::new("Name|Value", @("C|3", "D|4"))
$reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3"))
$actual = $DifferenceRender.RenderTableNodesDiff($reportItem)
$actual | Should -Be @'
#### Header 2 > Header 3
| Name | Value |
| ----- | ----- |
| ~~A~~ | ~~1~~ |
| ~~B~~ | ~~2~~ |
'@
}
It "Row is changed in existing table" {
$previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2"))
$currentNode = [TableNode]::new("Name|Value", @("A|1", "B|3"))
$reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3"))
$actual = $DifferenceRender.RenderTableNodesDiff($reportItem)
$actual | Should -Be @'
#### Header 2 > Header 3
| Name | Value |
| ----- | ----- |
| ~~B~~ | ~~2~~ |
| B | 3 |
'@
}
It "Row is changed, added and removed at the same time in existing table" {
$previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2", "C|3", "D|4"))
$currentNode = [TableNode]::new("Name|Value", @("B|2", "C|4", "D|4", "E|5"))
$reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3"))
$actual = $DifferenceRender.RenderTableNodesDiff($reportItem)
$actual | Should -Be @'
#### Header 2 > Header 3
| Name | Value |
| ----- | ----- |
| ~~A~~ | ~~1~~ |
| ~~C~~ | ~~3~~ |
| C | 4 |
| E | 5 |
'@
}
}
}
@@ -1,93 +0,0 @@
using module ../SoftwareReport.psm1
using module ../SoftwareReport.Nodes.psm1
Describe "SoftwareReport.E2E" {
Context "Report example 1" {
BeforeEach {
$softwareReport = [SoftwareReport]::new("macOS 11")
$softwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7 (20G817)")
$softwareReport.Root.AddToolVersion("Image Version:", "20220918.1")
$installedSoftware = $softwareReport.Root.AddHeader("Installed Software")
$languagesAndRuntimes = $installedSoftware.AddHeader("Language and Runtime")
$languagesAndRuntimes.AddToolVersion("Bash", "5.1.16(1)-release")
$languagesAndRuntimes.AddToolVersionsListInline(".NET Core SDK", @("1.2.100", "1.2.200", "3.1.414"), "^\d+\.\d+\.\d")
$languagesAndRuntimes.AddNode([ToolVersionNode]::new("Perl", "5.34.0"))
$cachedTools = $installedSoftware.AddHeader("Cached Tools")
$cachedTools.AddToolVersionsList("Ruby", @("2.7.3", "2.8.1", "3.1.2"), "^\d+\.\d+")
$cachedTools.AddToolVersionsList("Node.js", @("14.8.0", "15.1.0", "16.4.2"), "^\d+")
$javaSection = $installedSoftware.AddHeader("Java")
$javaSection.AddTable(@(
[PSCustomObject] @{ Version = "8.0.125"; Vendor = "My Vendor"; "Environment Variable" = "JAVA_HOME_8_X64" },
[PSCustomObject] @{ Version = "11.3.103"; Vendor = "My Vendor"; "Environment Variable" = "JAVA_HOME_11_X64" }
))
$sqlSection = $installedSoftware.AddHeader("MySQL")
$sqlSection.AddToolVersion("MySQL", "6.1.0")
$sqlSection.AddNote("MySQL service is disabled by default.`nUse the following command as a part of your job to start the service: 'sudo systemctl start mysql.service'")
$expectedMarkdown = @'
# macOS 11
- OS Version: macOS 11.7 (20G817)
- Image Version: 20220918.1
## Installed Software
### Language and Runtime
- Bash 5.1.16(1)-release
- .NET Core SDK: 1.2.100, 1.2.200, 3.1.414
- Perl 5.34.0
### Cached Tools
#### Ruby
- 2.7.3
- 2.8.1
- 3.1.2
#### Node.js
- 14.8.0
- 15.1.0
- 16.4.2
### Java
| Version | Vendor | Environment Variable |
| -------- | --------- | -------------------- |
| 8.0.125 | My Vendor | JAVA_HOME_8_X64 |
| 11.3.103 | My Vendor | JAVA_HOME_11_X64 |
### MySQL
- MySQL 6.1.0
```
MySQL service is disabled by default.
Use the following command as a part of your job to start the service: 'sudo systemctl start mysql.service'
```
'@
}
It "ToMarkdown" {
$softwareReport.ToMarkdown() | Should -Be $expectedMarkdown
}
It "Serialization + Deserialization" {
$json = $softwareReport.ToJson()
$deserializedReport = [SoftwareReport]::FromJson($json)
$deserializedReport.ToMarkdown() | Should -Be $expectedMarkdown
}
}
Context "GetImageVersion" {
It "Image version exists" {
$softwareReport = [SoftwareReport]::new("MyReport")
$softwareReport.Root.AddToolVersion("Image Version:", "123.4")
$softwareReport.GetImageVersion() | Should -Be "123.4"
}
It "Empty report" {
$softwareReport = [SoftwareReport]::new("MyReport")
$softwareReport.GetImageVersion() | Should -Be "Unknown version"
}
}
}
@@ -1,511 +0,0 @@
using module ../SoftwareReport.Nodes.psm1
BeforeDiscovery {
Import-Module $(Join-Path $PSScriptRoot "TestHelpers.psm1") -DisableNameChecking
}
Describe "Nodes.UnitTests" {
Context "ToolVersionNode" {
It "ToMarkdown" {
$node = [ToolVersionNode]::new("MyTool", "2.1.3")
$node.ToMarkdown() | Should -Be "- MyTool 2.1.3"
}
It "GetValue" {
$node = [ToolVersionNode]::new("MyTool", "2.1.3")
$node.GetValue() | Should -Be "2.1.3"
}
It "Serialization" {
$node = [ToolVersionNode]::new("MyTool", "2.1.3")
$json = $node.ToJsonObject()
$json.NodeType | Should -Be "ToolVersionNode"
$json.ToolName | Should -Be "MyTool"
$json.Version | Should -Be "2.1.3"
}
It "Deserialization" {
{ [ToolVersionNode]::FromJsonObject(@{ NodeType = "ToolVersionNode"; ToolName = ""; Version = "2.1.3" }) } | Should -Throw '*Exception setting "ToolName": "The argument is null or empty.*'
{ [ToolVersionNode]::FromJsonObject(@{ NodeType = "ToolVersionNode"; ToolName = "MyTool"; Version = "" }) } | Should -Throw 'ToolVersionNode ''MyTool'' has empty version'
{ [ToolVersionNode]::FromJsonObject(@{ NodeType = "ToolVersionNode"; ToolName = "MyTool"; Version = "2.1.3" }) } | Should -Not -Throw
}
It "Serialization + Deserialization" {
$node = [ToolVersionNode]::new("MyTool", "2.1.3")
$json = $node.ToJsonObject()
$node2 = [ToolVersionNode]::FromJsonObject($json)
$json2 = $node2.ToJsonObject()
$($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json)
}
It "IsSimilarTo" {
[ToolVersionNode]::new("MyTool", "2.1.3").IsSimilarTo([ToolVersionNode]::new("MyTool", "2.1.3")) | Should -BeTrue
[ToolVersionNode]::new("MyTool", "2.1.3").IsSimilarTo([ToolVersionNode]::new("MyTool", "1.0.0")) | Should -BeTrue
[ToolVersionNode]::new("MyTool", "2.1.3").IsSimilarTo([ToolVersionNode]::new("MyTool2", "2.1.3")) | Should -BeFalse
}
It "IsIdenticalTo" {
[ToolVersionNode]::new("MyTool", "2.1.3").IsIdenticalTo([ToolVersionNode]::new("MyTool", "2.1.3")) | Should -BeTrue
[ToolVersionNode]::new("MyTool", "2.1.3").IsIdenticalTo([ToolVersionNode]::new("MyTool", "1.0.0")) | Should -BeFalse
[ToolVersionNode]::new("MyTool", "2.1.3").IsIdenticalTo([ToolVersionNode]::new("MyTool2", "2.1.3")) | Should -BeFalse
}
}
Context "ToolVersionsListNode" {
It "ToMarkdown - List" {
$node = [ToolVersionsListNode]::new("MyTool", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "List")
$expected = @(
"",
"# MyTool"
"- 2.7.7"
"- 3.0.5"
"- 3.1.3"
) -join "`n"
$node.ToMarkdown() | Should -Be $expected
}
It "ToMarkdown - Inline" {
$node = [ToolVersionsListNode]::new("MyTool", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "Inline")
$node.ToMarkdown() | Should -Be "- MyTool: 2.7.7, 3.0.5, 3.1.3"
}
It "GetValue" {
$node = [ToolVersionsListNode]::new("MyTool", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "List")
$node.GetValue() | Should -Be "2.7.7, 3.0.5, 3.1.3"
}
It "Serialization - List" {
$node = [ToolVersionsListNode]::new("Ruby", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "List")
$json = $node.ToJsonObject()
$json.NodeType | Should -Be "ToolVersionsListNode"
$json.ToolName | Should -Be "Ruby"
$json.Versions | Should -BeArray @("2.7.7", "3.0.5", "3.1.3")
$json.MajorVersionRegex | Should -Be "^.+"
$json.ListType | Should -Be "List"
}
It "Serialization - Inline" {
$node = [ToolVersionsListNode]::new("Ruby", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "Inline")
$json = $node.ToJsonObject()
$json.NodeType | Should -Be "ToolVersionsListNode"
$json.ToolName | Should -Be "Ruby"
$json.Versions | Should -BeArray @("2.7.7", "3.0.5", "3.1.3")
$json.MajorVersionRegex | Should -Be "^.+"
$json.ListType | Should -Be "Inline"
}
It "Deserialization" {
{ [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = ""; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Throw '*Exception setting "ToolName": "The argument is null or empty.*'
{ [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Throw '*Exception setting "Versions": "The argument is null or empty.*'
{ [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @(); MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Throw '*Exception setting "Versions": "The argument is null, empty,*'
{ [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", '2.2.4'); MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Throw 'Multiple versions from list * return the same result from regex *'
{ [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = ""; ListType = "List" }) } | Should -Throw 'Version * doesn''t match regex *'
{ [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = "^\d+"; ListType = "Fake" }) } | Should -Throw '*Exception setting "ListType": "The argument * does not belong to the set*'
{ [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Not -Throw
{ [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = "^\d+"; ListType = "Inline" }) } | Should -Not -Throw
}
It "Serialization + Deserialization" {
$node = [ToolVersionsListNode]::new("Ruby", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "List")
$json = $node.ToJsonObject()
$node2 = [ToolVersionsListNode]::FromJsonObject($json)
$json2 = $node2.ToJsonObject()
$($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json)
}
It "IsSimilarTo" {
[ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsSimilarTo(
[ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List")
) | Should -BeTrue
[ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsSimilarTo(
[ToolVersionsListNode]::new("MyTool", @("2.1.5", "5.0.0"), "^.+", "List")
) | Should -BeTrue
[ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsSimilarTo(
[ToolVersionsListNode]::new("MyTool2", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List")
) | Should -BeFalse
}
It "IsIdenticalTo" {
[ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsIdenticalTo(
[ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List")
) | Should -BeTrue
[ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsIdenticalTo(
[ToolVersionsListNode]::new("MyTool", @("2.1.5", "5.0.0"), "^.+", "List")
) | Should -BeFalse
[ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsIdenticalTo(
[ToolVersionsListNode]::new("MyTool2", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List")
) | Should -BeFalse
}
It "ExtractMajorVersion" {
$node = [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^\d+\.\d+", "List")
$node.ExtractMajorVersion("2.1.3") | Should -Be "2.1"
$node.ExtractMajorVersion("3.1.5") | Should -Be "3.1"
$node.ExtractMajorVersion("4.0.0") | Should -Be "4.0"
}
Context "ValidateMajorVersionRegex" {
It "Major version regex - unique versions" {
$node = [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^\d+", "List")
$node.Versions | Should -BeArray @("2.1.3", "3.1.5", "4.0.0")
}
It "Major version regex - non-unique versions" {
{ [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "3.2.0", "4.0.0"), "^\d+", "List") } | Should -Throw "Multiple versions from list * return the same result from regex *"
}
It "Minor version regex - unique versions" {
$node = [ToolVersionsListNode]::new("MyTool", @("2.1.3", "2.4.0", "3.1.2"), "^\d+\.\d+", "List")
$node.Versions | Should -BeArray @("2.1.3", "2.4.0", "3.1.2")
}
It "Minor version regex - non-unique versions" {
{ [ToolVersionsListNode]::new("MyTool", @("2.1.3", "2.1.4", "3.1.2"), "^\d+\.\d+", "List") } | Should -Throw "Multiple versions from list * return the same result from regex *"
}
It "Patch version regex - unique versions" {
$node = [ToolVersionsListNode]::new("MyTool", @("2.1.3", "2.1.4", "2.1.5"), "^\d+\.\d+\.\d+", "List")
$node.Versions | Should -BeArray @("2.1.3", "2.1.4", "2.1.5")
}
It "Patch version regex - non-unique versions" {
{ [ToolVersionsListNode]::new("MyTool", @("2.1.3", "2.1.4", "2.1.4"), "^\d+\.\d+\.\d+", "List") } | Should -Throw "Multiple versions from list * return the same result from regex *"
}
It ".NET Core version regex - unique versions" {
$node = [ToolVersionsListNode]::new("MyTool", @("2.1.100", "2.1.205", "2.1.303"), "^\d+\.\d+\.\d", "List")
$node.Versions | Should -BeArray @("2.1.100", "2.1.205", "2.1.303")
}
It ".NET Core version regex - non-unique versions" {
{ [ToolVersionsListNode]::new("MyTool", @("2.1.100", "2.1.205", "2.1.230", "3.1.0"), "^\d+\.\d+\.\d", "List") } | Should -Throw "Multiple versions from list * return the same result from regex *"
}
}
}
Context "TableNode" {
It "ToMarkdown (Simple table)" {
$node = [TableNode]::new("Name|Value", @("A|B", "C|D"))
$node.ToMarkdown() | Should -Be @'
| Name | Value |
| ---- | ----- |
| A | B |
| C | D |
'@
}
It "ToMarkdown (Wide cells)" {
$node = [TableNode]::new("Name|Value", @("Very long value here|B", "C|And very long value here too"))
$node.ToMarkdown() | Should -Be @'
| Name | Value |
| -------------------- | ---------------------------- |
| Very long value here | B |
| C | And very long value here too |
'@
}
It "CalculateColumnsWidth" {
[TableNode]::new("Name|Value", @("A|B", "C|D")).CalculateColumnsWidth() | Should -BeArray @(4, 5)
[TableNode]::new("Name|Value", @("Very long value here|B", "C|And very long value here too")).CalculateColumnsWidth() | Should -BeArray @(20, 28)
}
It "Serialization" {
$node = [TableNode]::new("Name|Value", @("A|B", "C|D"))
$json = $node.ToJsonObject()
$json.NodeType | Should -Be "TableNode"
$json.Headers | Should -Be "Name|Value"
$json.Rows | Should -BeArray @("A|B", "C|D")
}
It "Deserialization" {
{ [TableNode]::FromJsonObject(@{ NodeType = "TableNode"; Headers = ""; Rows = @("A|1", "B|2") }) } | Should -Throw 'Exception setting "Headers": "The argument is null or empty. *'
{ [TableNode]::FromJsonObject(@{ NodeType = "TableNode"; Headers = "Name|Value"; Rows = @() }) } | Should -Throw 'Exception setting "Rows": "The argument is null, empty, *'
{ [TableNode]::FromJsonObject(@{ NodeType = "TableNode"; Headers = "Name|Value"; Rows = @("A|1", "B|2|T", "C|3") }) } | Should -Throw 'Table has different number of columns in different rows'
{ [TableNode]::FromJsonObject(@{ NodeType = "TableNode"; Headers = "Name|Value"; Rows = @("A|1", "B|2") }) } | Should -Not -Throw
}
It "Serialization + Deserialization" {
$node = [TableNode]::new("Name|Value", @("A|B", "C|D"))
$json = $node.ToJsonObject()
$node2 = [TableNode]::FromJsonObject($json)
$json2 = $node2.ToJsonObject()
$($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json)
}
It "IsSimilarTo" {
[TableNode]::new("Name|Value", @("A|B", "C|D")).IsSimilarTo([TableNode]::new("Name|Value", @("A|B", "C|D"))) | Should -BeTrue
[TableNode]::new("Name|Value", @("A|B", "C|D")).IsSimilarTo([TableNode]::new("Name|Value", @("A|B", "C|D", "F|W"))) | Should -BeTrue
[TableNode]::new("Name|Value", @("A|B", "C|D")).IsSimilarTo([TableNode]::new("Name|Value", @("A|B", "C|E"))) | Should -BeTrue
[TableNode]::new("Name|Value", @("A|B", "C|D")).IsSimilarTo([TableNode]::new("Name|Key", @("A|B", "C|D"))) | Should -BeTrue
}
It "IsIdenticalTo" {
[TableNode]::new("Name|Value", @("A|B", "C|D")).IsIdenticalTo([TableNode]::new("Name|Value", @("A|B", "C|D"))) | Should -BeTrue
[TableNode]::new("Name|Value", @("A|B", "C|D")).IsIdenticalTo([TableNode]::new("Name|Key", @("A|B", "C|D"))) | Should -BeTrue
[TableNode]::new("Name|Value", @("A|B", "C|D")).IsIdenticalTo([TableNode]::new("Name|Value", @("A|B", "C|D", "F|W"))) | Should -BeFalse
[TableNode]::new("Name|Value", @("A|B", "C|D")).IsIdenticalTo([TableNode]::new("Name|Value", @("A|B", "C|E"))) | Should -BeFalse
}
Context "FromObjectsArray" {
It "Correct table" {
$table = @(
[PSCustomObject]@{Name = "A"; Value = "B"}
[PSCustomObject]@{Name = "C"; Value = "D"}
)
$tableNode = [TableNode]::FromObjectsArray($table)
$tableNode.Headers | Should -Be "Name|Value"
$tableNode.Rows | Should -BeArray @("A|B", "C|D")
}
It "Correct table with spaces" {
$table = @(
[PSCustomObject]@{Name = "A B"; "My Value" = "1 2"}
[PSCustomObject]@{Name = "C D"; "My Value" = "3 4"}
)
$tableNode = [TableNode]::FromObjectsArray($table)
$tableNode.Headers | Should -Be "Name|My Value"
$tableNode.Rows | Should -BeArray @("A B|1 2", "C D|3 4")
}
It "Throw on empty table" {
{ [TableNode]::FromObjectsArray(@()) } | Should -Throw "Failed to create TableNode from empty objects array"
}
It "Throw on table with different columns" {
$table = @(
[PSCustomObject]@{Name = "A"; Value = "B"}
[PSCustomObject]@{Name = "C"; Value2 = "D"}
)
{ [TableNode]::FromObjectsArray($table) } | Should -Throw "Failed to create TableNode from objects array because objects have different properties"
}
It "Throw on empty row" {
$table = @(
[PSCustomObject]@{Name = "A"; Value = "B"},
[PSCustomObject]@{},
[PSCustomObject]@{Name = "C"; Value2 = "D"}
)
{ [TableNode]::FromObjectsArray($table) } | Should -Throw "Failed to create TableNode because some objects are empty"
}
It "Throw on incorrect symbols in table column names" {
$table = @(
[PSCustomObject]@{"Name|War" = "A"; Value = "B"}
[PSCustomObject]@{"Name|War" = "C"; Value = "D"}
)
{ [TableNode]::FromObjectsArray($table) } | Should -Throw "Failed to create TableNode because some cells * contains forbidden symbol*"
}
It "Throw on incorrect symbols in table rows" {
$table = @(
[PSCustomObject]@{Name = "A"; Value = "B|AA"}
[PSCustomObject]@{Name = "C"; Value = "D"}
)
{ [TableNode]::FromObjectsArray($table) } | Should -Throw "Failed to create TableNode because some cells * contains forbidden symbol*"
}
}
}
Context "NoteNode" {
It "ToMarkdown" {
$node = [NoteNode]::new("Hello world`nGood Bye world")
$node.ToMarkdown() | Should -Be @'
```
hello world
Good Bye world
```
'@
}
It "Serialization" {
$node = [NoteNode]::new("MyContent`nMyContent2")
$json = $node.ToJsonObject()
$json.NodeType | Should -Be "NoteNode"
$json.Content | Should -Be "MyContent`nMyContent2"
}
It "Deserialization" {
{ [NoteNode]::FromJsonObject(@{ NodeType = "NoteNode" }) } | Should -Throw '*Exception setting "Content": "The argument is null or empty.*'
{ [NoteNode]::FromJsonObject(@{ NodeType = "NoteNode"; Content = "" }) } | Should -Throw '*Exception setting "Content": "The argument is null or empty.*'
{ [NoteNode]::FromJsonObject(@{ NodeType = "NoteNode"; Content = "MyTool" }) } | Should -Not -Throw
}
It "Serialization + Deserialization" {
$node = [NoteNode]::new("MyContent`nMyContent2")
$json = $node.ToJsonObject()
$node2 = [NoteNode]::FromJsonObject($json)
$json2 = $node2.ToJsonObject()
$($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json)
}
It "IsSimilarTo" {
[NoteNode]::new("MyContent").IsSimilarTo([NoteNode]::new("MyContent")) | Should -BeTrue
[NoteNode]::new("MyContent").IsSimilarTo([NoteNode]::new("MyContent2")) | Should -BeFalse
}
It "IsIdenticalTo" {
[NoteNode]::new("MyContent").IsIdenticalTo([NoteNode]::new("MyContent")) | Should -BeTrue
[NoteNode]::new("MyContent").IsIdenticalTo([NoteNode]::new("MyContent2")) | Should -BeFalse
}
}
Context "HeaderNode" {
It "ToMarkdown" {
$node = [HeaderNode]::new("MyHeader")
$node.AddToolVersion("MyTool", "2.1.3")
$node.ToMarkdown(1) | Should -Be @'
# MyHeader
- MyTool 2.1.3
'@
}
It "ToMarkdown (level 3)" {
$node = [HeaderNode]::new("MyHeader")
$node.AddToolVersion("MyTool", "2.1.3")
$node.ToMarkdown(3) | Should -Be @'
### MyHeader
- MyTool 2.1.3
'@
}
It "ToMarkdown (multiple levels)" {
$node = [HeaderNode]::new("MyHeader")
$node.AddHeader("MyHeader 2").AddHeader("MyHeader 3").AddHeader("MyHeader 4").AddToolVersion("MyTool", "2.1.3")
$node.ToMarkdown(1) | Should -Be @'
# MyHeader
## MyHeader 2
### MyHeader 3
#### MyHeader 4
- MyTool 2.1.3
'@
}
It "Serialization" {
$node = [HeaderNode]::new("MyHeader")
$node.AddToolVersion("MyTool", "2.1.3")
$json = $node.ToJsonObject()
$json.NodeType | Should -Be "HeaderNode"
$json.Title | Should -Be "MyHeader"
$json.Children | Should -HaveCount 1
}
It "Deserialization" {
{ [HeaderNode]::FromJsonObject(@{ NodeType = "HeaderNode" }) } | Should -Throw '*Exception setting "Title": "The argument is null or empty.*'
{ [HeaderNode]::FromJsonObject(@{ NodeType = "HeaderNode"; Title = "" }) } | Should -Throw '*Exception setting "Title": "The argument is null or empty.*'
{ [HeaderNode]::FromJsonObject(@{ NodeType = "HeaderNode"; Title = "MyHeader" }) } | Should -Not -Throw
}
It "Serialization + Deserialization" {
$node = [HeaderNode]::new("MyHeader")
$node.AddToolVersion("MyTool", "2.1.3")
$json = $node.ToJsonObject()
$node2 = [HeaderNode]::FromJsonObject($json)
$json2 = $node2.ToJsonObject()
$($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json)
}
It "IsSimilarTo" {
[HeaderNode]::new("MyHeader").IsSimilarTo([HeaderNode]::new("MyHeader")) | Should -BeTrue
[HeaderNode]::new("MyHeader").IsSimilarTo([HeaderNode]::new("MyHeader2")) | Should -BeFalse
}
It "IsIdenticalTo" {
[HeaderNode]::new("MyHeader").IsIdenticalTo([HeaderNode]::new("MyHeader")) | Should -BeTrue
[HeaderNode]::new("MyHeader").IsIdenticalTo([HeaderNode]::new("MyHeader2")) | Should -BeFalse
}
It "FindSimilarChildNode" {
$node = [HeaderNode]::new("MyHeader")
$node.AddToolVersion("MyTool", "2.1.3")
$node.FindSimilarChildNode([ToolVersionNode]::new("MyTool", "1.0.0")) | Should -Not -BeNullOrEmpty
$node.FindSimilarChildNode([ToolVersionNode]::New("MyTool2", "1.0.0")) | Should -BeNullOrEmpty
}
Context "Detect node duplicates" {
It "Similar HeaderNode on the same header" {
$node = [HeaderNode]::new("MyHeader")
$node.AddHeader("MySubHeader1")
$node.AddHeader("MySubHeader2")
{ $node.AddHeader("MySubHeader1") } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*"
}
It "Similar ToolVersionNode on the same header" {
$node = [HeaderNode]::new("MyHeader")
$node.AddToolVersion("MyTool", "2.1.3")
$node.AddToolVersion("MyTool2", "2.1.3")
{ $node.AddToolVersion("MyTool", "2.1.3") } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*"
}
It "Similar ToolVersionsListNode on the same header" {
$node = [HeaderNode]::new("MyHeader")
$node.AddToolVersionsListInline("MyTool", @("2.1.3", "3.0.0"), "^\d+")
$node.AddToolVersionsListInline("MyTool2", @("2.1.3", "3.0.0"), "^\d+")
{ $node.AddToolVersionsList("MyTool", @("2.1.3", "3.0.0"), "^\d+") } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*"
}
It "Similar TableNode on the same header" {
$node = [HeaderNode]::new("MyHeader")
$node.AddTable(@(
[PSCustomObject]@{Name = "Value1"},
[PSCustomObject]@{Name = "Value2"}
))
{
$node.AddTable(@(
[PSCustomObject]@{Name = "Value1"},
[PSCustomObject]@{Name = "Value2"}
))
} | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*"
}
It "Similar NoteNode on the same header" {
$node = [HeaderNode]::new("MyHeader")
$node.AddNote("MyContent")
$node.AddNote("MyContent2")
{ $node.AddNote("MyContent") } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*"
}
It "AddNode detects duplicates" {
$node = [HeaderNode]::new("MyHeader")
$node.AddNode([ToolVersionNode]::new("MyTool", "2.1.3"))
{ $node.AddNode([ToolVersionNode]::new("MyTool", "2.1.3")) } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*"
}
It "AddNodes detects duplicates" {
$node = [HeaderNode]::new("MyHeader")
$node.AddNodes(@(
[ToolVersionNode]::new("MyTool", "2.1.3"),
[ToolVersionNode]::new("MyTool2", "2.1.4")
))
{
$node.AddNodes(@(
[ToolVersionNode]::new("MyTool3", "2.1.5"),
[ToolVersionNode]::new("MyTool", "2.1.3")
))
} | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*"
}
It "Doesn't allow adding non-header nodes after header node" {
$node = [HeaderNode]::new("MyHeader")
{ $node.AddToolVersion("MyTool", "2.1.3") } | Should -Not -Throw
{ $node.AddHeader("MySubHeader") } | Should -Not -Throw
{ $node.AddToolVersion("MyTool2", "2.1.4") } | Should -Throw "It is not allowed to add the non-header node after the header node. Consider adding the separate HeaderNode for this node"
{ $node.AddHeader("MySubHeader2") } | Should -Not -Throw
{ $node.AddToolVersionsListInline("MyTool3", @("2.1.4", "2.1.5"), "^.+") } | Should -Throw "It is not allowed to add the non-header node after the header node. Consider adding the separate HeaderNode for this node"
{ $node.AddToolVersionsList("MyTool4", @("2.1.4", "2.1.5"), "^.+") } | Should -Not -Throw
}
}
}
}
@@ -1,34 +0,0 @@
function ShouldBeArray([Array] $ActualValue, [Array]$ExpectedValue, [Switch] $Negate, [String] $Because) {
if ($Negate) {
throw "Negation is not supported for Should-BeArray"
}
if ($ExpectedValue.Count -eq 0) {
throw "Expected array cannot be empty. Use Should-BeNullOrEmpty instead."
}
$ExpectedValue | ForEach-Object {
if ($_.GetType() -notin @([String], [Int32])) {
throw "Only string or int arrays are supported in Should-BeArray"
}
}
$actualValueJson = $ActualValue | ConvertTo-Json
$expectedValueJson = $ExpectedValue | ConvertTo-Json
$succeeded = ($ActualValue.Count -eq $ExpectedValue.Count) -and ($actualValueJson -eq $expectedValueJson)
if (-not $succeeded) {
$failureMessage = "Expected array '$actualValueJson' to be equal to '$expectedValueJson'"
}
return [PSCustomObject]@{
Succeeded = $succeeded
FailureMessage = $failureMessage
}
}
Add-ShouldOperator -Name BeArray `
-InternalName 'ShouldBeArray' `
-Test ${function:ShouldBeArray} `
-SupportsArrayInput
+14
View File
@@ -0,0 +1,14 @@
param(
[String] [Parameter (Mandatory=$true)] $RepoUrl,
[String] [Parameter (Mandatory=$true)] $RepoBranch
)
Write-Host "Clean up default repository"
Remove-Item -path './*' -Recurse -Force
Write-Host "Download ${RepoBranch} branch from ${RepoUrl}"
$env:GIT_REDIRECT_STDERR = '2>&1'
git clone $RepoUrl . -b $RepoBranch --single-branch --depth 1
Write-Host "Latest commit:"
git --no-pager log --pretty=format:"Date: %cd; Commit: %H - %s; Author: %an <%ae>" -1
@@ -0,0 +1,100 @@
# Ideally we would use GitHub Actions for this, but since we use self-hosted machines to run image builds
# we need the following features to use GitHub Actions for Images CI:
# - https://github.community/t5/GitHub-Actions/Make-secrets-available-to-builds-of-forks/m-p/30678#M508
# - https://github.community/t5/GitHub-Actions/GitHub-Actions-Manual-Trigger-Approvals/td-p/31504
# - https://github.community/t5/GitHub-Actions/Protecting-github-workflows/td-p/30290
jobs:
- job:
displayName: Image Generation (${{ parameters.image_type }})
timeoutInMinutes: 600
cancelTimeoutInMinutes: 30
pool: ci-agent-pool
variables:
- group: Image Generation Variables
steps:
- task: PowerShell@2
displayName: 'Download custom repository'
condition: and(ne(variables['CUSTOM_REPOSITORY_URL'], ''), ne(variables['CUSTOM_REPOSITORY_BRANCH'], ''))
inputs:
targetType: 'filePath'
filePath: ./images.CI/download-repo.ps1
arguments: -RepoUrl $(CUSTOM_REPOSITORY_URL) `
-RepoBranch $(CUSTOM_REPOSITORY_BRANCH)
- task: PowerShell@2
displayName: 'Set image template variables'
inputs:
targetType: 'inline'
script: |
$ImageType = "${{ parameters.image_type }}"
$TemplateDirectoryName = if ($ImageType.StartsWith("ubuntu")) { "linux" } else { "win" }
$TemplateDirectoryPath = Join-Path "images" $TemplateDirectoryName | Resolve-Path
$TemplatePath = Join-Path $TemplateDirectoryPath "$ImageType.json"
Write-Host "##vso[task.setvariable variable=TemplateDirectoryPath;]$TemplateDirectoryPath"
Write-Host "##vso[task.setvariable variable=TemplatePath;]$TemplatePath"
- task: PowerShell@2
displayName: 'Build VM'
inputs:
targetType: filePath
filePath: ./images.CI/linux-and-win/build-image.ps1
arguments: -ResourcesNamePrefix $(Build.BuildId) `
-ClientId $(CLIENT_ID) `
-ClientSecret $(CLIENT_SECRET) `
-TemplatePath $(TemplatePath) `
-ResourceGroup $(AZURE_RESOURCE_GROUP) `
-StorageAccount $(AZURE_STORAGE_ACCOUNT) `
-SubscriptionId $(AZURE_SUBSCRIPTION) `
-TenantId $(AZURE_TENANT) `
-Location $(AZURE_LOCATION) `
-VirtualNetworkName $(BUILD_AGENT_VNET_NAME) `
-VirtualNetworkRG $(BUILD_AGENT_VNET_RESOURCE_GROUP) `
-VirtualNetworkSubnet $(BUILD_AGENT_SUBNET_NAME)
env:
PACKER_LOG: 1
PACKER_LOG_PATH: $(Build.ArtifactStagingDirectory)/packer-log.txt
- task: PowerShell@2
displayName: 'Output Readme file content'
inputs:
targetType: 'inline'
script: |
Get-Content -Path (Join-Path "$(TemplateDirectoryPath)" "${{ parameters.image_readme_name }}")
- task: PowerShell@2
displayName: 'Print provisioners duration'
inputs:
targetType: 'filePath'
filePath: ./images.CI/measure-provisioners-duration.ps1
arguments: -PackerLogPath "$(Build.ArtifactStagingDirectory)/packer-log.txt" `
-PrefixToPathTrim "$(TemplateDirectoryPath)" `
-PrintTopNLongest 25
- task: PowerShell@2
displayName: 'Create release for VM deployment'
inputs:
targetType: filePath
filePath: ./images.CI/linux-and-win/create-release.ps1
arguments: -BuildId $(Build.BuildId) `
-Organization $(RELEASE_TARGET_ORGANIZATION) `
-DefinitionId $(RELEASE_TARGET_DEFINITION_ID) `
-Project $(RELEASE_TARGET_PROJECT) `
-ImageName ${{ parameters.image_type }} `
-AccessToken $(RELEASE_TARGET_TOKEN)
- task: PowerShell@2
displayName: 'Clean up resources'
condition: always()
inputs:
targetType: filePath
filePath: ./images.CI/linux-and-win/cleanup.ps1
arguments: -ResourcesNamePrefix $(Build.BuildId) `
-Image ${{ parameters.image_type }} `
-StorageAccount $(AZURE_STORAGE_ACCOUNT) `
-SubscriptionId $(AZURE_SUBSCRIPTION) `
-ClientId $(CLIENT_ID) `
-ClientSecret $(CLIENT_SECRET) `
-TenantId $(AZURE_TENANT)
@@ -0,0 +1,20 @@
schedules:
- cron: "0 0 * * *"
displayName: Daily
branches:
include:
- main
always: true
trigger: none
pr:
autoCancel: true
branches:
include:
- main
jobs:
- template: image-generation.yml
parameters:
image_type: ubuntu1804
image_readme_name: Ubuntu1804-Readme.md
@@ -0,0 +1,20 @@
schedules:
- cron: "0 0 * * *"
displayName: Daily
branches:
include:
- main
always: true
trigger: none
pr:
autoCancel: true
branches:
include:
- main
jobs:
- template: image-generation.yml
parameters:
image_type: ubuntu2004
image_readme_name: Ubuntu2004-Readme.md
@@ -0,0 +1,20 @@
schedules:
- cron: "0 0 * * *"
displayName: Daily
branches:
include:
- main
always: true
trigger: none
pr:
autoCancel: true
branches:
include:
- main
jobs:
- template: image-generation.yml
parameters:
image_type: windows2016
image_readme_name: Windows2016-Readme.md
@@ -0,0 +1,20 @@
schedules:
- cron: "0 0 * * *"
displayName: Daily
branches:
include:
- main
always: true
trigger: none
pr:
autoCancel: true
branches:
include:
- main
jobs:
- template: image-generation.yml
parameters:
image_type: windows2019
image_readme_name: Windows2019-Readme.md
@@ -0,0 +1,20 @@
schedules:
- cron: "0 0 * * *"
displayName: Daily
branches:
include:
- main
always: true
trigger: none
pr:
autoCancel: true
branches:
include:
- main
jobs:
- template: image-generation.yml
parameters:
image_type: windows2022
image_readme_name: Windows2022-Readme.md
+17 -32
View File
@@ -1,22 +1,16 @@
param(
[String] [Parameter (Mandatory=$true)] $TemplatePath,
[String] [Parameter (Mandatory=$true)] $BuildTemplateName,
[String] [Parameter (Mandatory=$true)] $ClientId,
[String] [Parameter (Mandatory=$false)] $ClientSecret,
[String] [Parameter (Mandatory=$true)] $ClientSecret,
[String] [Parameter (Mandatory=$true)] $ResourcesNamePrefix,
[String] [Parameter (Mandatory=$true)] $Location,
[String] [Parameter (Mandatory=$true)] $ImageName,
[String] [Parameter (Mandatory=$true)] $ImageResourceGroupName,
[String] [Parameter (Mandatory=$true)] $TempResourceGroupName,
[String] [Parameter (Mandatory=$true)] $ResourceGroup,
[String] [Parameter (Mandatory=$true)] $StorageAccount,
[String] [Parameter (Mandatory=$true)] $SubscriptionId,
[String] [Parameter (Mandatory=$true)] $TenantId,
[String] [Parameter (Mandatory=$true)] $ImageOS, # e.g. "ubuntu22", "ubuntu22" or "win19", "win22", "win25"
[String] [Parameter (Mandatory=$false)] $UseAzureCliAuth = "false",
[String] [Parameter (Mandatory=$false)] $PluginVersion = "2.3.3",
[String] [Parameter (Mandatory=$false)] $VirtualNetworkName,
[String] [Parameter (Mandatory=$false)] $VirtualNetworkRG,
[String] [Parameter (Mandatory=$false)] $VirtualNetworkSubnet,
[String] [Parameter (Mandatory=$false)] $AllowedInboundIpAddresses = "[]",
[hashtable] [Parameter (Mandatory=$false)] $Tags = @{}
[String] [Parameter (Mandatory=$true)] $VirtualNetworkName,
[String] [Parameter (Mandatory=$true)] $VirtualNetworkRG,
[String] [Parameter (Mandatory=$true)] $VirtualNetworkSubnet
)
if (-not (Test-Path $TemplatePath))
@@ -25,9 +19,12 @@ if (-not (Test-Path $TemplatePath))
exit 1
}
$buildName = $($BuildTemplateName).Split(".")[1]
$Image = [io.path]::GetFileNameWithoutExtension($TemplatePath)
$TempResourceGroupName = "${ResourcesNamePrefix}_${Image}"
$InstallPassword = [System.GUID]::NewGuid().ToString().ToUpper()
packer validate -syntax-only $TemplatePath
$SensitiveData = @(
'OSType',
'StorageAccountLocation',
@@ -38,40 +35,28 @@ $SensitiveData = @(
': ->'
)
$azure_tags = $Tags | ConvertTo-Json -Compress
Write-Host "Show Packer Version"
packer --version
Write-Host "Download packer plugins"
packer plugins install github.com/hashicorp/azure $pluginVersion
Write-Host "Validate packer template"
packer validate -syntax-only -only "$buildName*" $TemplatePath
Write-Host "Build $buildName VM"
packer build -only "$buildName*" `
Write-Host "Build $Image VM"
packer build -var "capture_name_prefix=$ResourcesNamePrefix" `
-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 "resource_group=$ResourceGroup" `
-var "storage_account=$StorageAccount" `
-var "subscription_id=$SubscriptionId" `
-var "temp_resource_group_name=$TempResourceGroupName" `
-var "tenant_id=$TenantId" `
-var "virtual_network_name=$VirtualNetworkName" `
-var "virtual_network_resource_group_name=$VirtualNetworkRG" `
-var "virtual_network_subnet_name=$VirtualNetworkSubnet" `
-var "allowed_inbound_ip_addresses=$($AllowedInboundIpAddresses)" `
-var "use_azure_cli_auth=$UseAzureCliAuth" `
-var "azure_tags=$azure_tags" `
-color=false `
-var "run_validation_diskspace=$env:RUN_VALIDATION_FLAG" `
$TemplatePath `
| Where-Object {
#Filter sensitive data from Packer logs
$currentString = $_
$sensitiveString = $SensitiveData | Where-Object { $currentString -match $_ }
$sensitiveString -eq $null
}
}
+19 -5
View File
@@ -1,12 +1,26 @@
param(
[Parameter (Mandatory=$true)] [string] $TempResourceGroupName
[String] [Parameter (Mandatory=$true)] $Image,
[String] [Parameter (Mandatory=$true)] $ResourcesNamePrefix,
[String] [Parameter (Mandatory=$true)] $StorageAccount,
[String] [Parameter (Mandatory=$true)] $ClientId,
[String] [Parameter (Mandatory=$true)] $ClientSecret,
[String] [Parameter (Mandatory=$true)] $SubscriptionId,
[String] [Parameter (Mandatory=$true)] $TenantId
)
$groupExist = az group exists --name $TempResourceGroupName
az login --service-principal --username $ClientId --password $ClientSecret --tenant $TenantId | Out-Null
$TempResourceGroupName = "${ResourcesNamePrefix}_${Image}"
$groupExist = az group exists --name $TempResourceGroupName --subscription $SubscriptionId
if ($groupExist -eq "true") {
$osDiskName = az group deployment list --resource-group $TempResourceGroupName --query "[].properties.parameters.osDiskName.value" -o tsv
Write-Host "Found a match, deleting temporary files"
az group delete --name $TempResourceGroupName --yes | Out-Null
Write-Host "Temporary group was deleted successfully"
az group delete --name $TempResourceGroupName --subscription $SubscriptionId --yes | Out-Null
Write-Host "Temporary group was deleted succesfully"
Write-Host "Deleting OS disk"
az storage remove --account-name $StorageAccount -c "images" -n "$osDiskName.vhd" --only-show-errors | Out-Null
Write-Host "OS disk deleted"
} else {
Write-Host "No temporary groups found"
}
}
+9 -14
View File
@@ -1,11 +1,10 @@
param(
[Parameter (Mandatory)] [UInt32] $BuildId,
[Parameter (Mandatory)] [string] $Organization,
[Parameter (Mandatory)] [string] $Project,
[Parameter (Mandatory)] [string] $ImageType,
[Parameter (Mandatory)] [string] $ManagedImageName,
[Parameter (Mandatory)] [string] $DefinitionId,
[Parameter (Mandatory)] [string] $AccessToken
[UInt32] [Parameter (Mandatory)] $BuildId,
[String] [Parameter (Mandatory)] $Organization,
[String] [Parameter (Mandatory)] $Project,
[String] [Parameter (Mandatory)] $ImageName,
[String] [Parameter (Mandatory)] $DefinitionId,
[String] [Parameter (Mandatory)] $AccessToken
)
$Body = @{
@@ -14,11 +13,8 @@ $Body = @{
ImageBuildId = @{
value = $BuildId
}
ImageType = @{
value = $ImageType
}
ManagedImageName = @{
value = $ManagedImageName
ImageName = @{
value = $ImageName
}
}
isDraft = "false"
@@ -30,7 +26,6 @@ $headers = @{
Authorization = "Basic ${base64AuthInfo}"
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
$NewRelease = Invoke-RestMethod $URL -Body $Body -Method "POST" -Headers $headers -ContentType "application/json"
Write-Host "Created release: $($NewRelease._links.web.href)"
Write-Host "Created release: $($NewRelease._links.web.href)"
+227
View File
@@ -0,0 +1,227 @@
function Push-AnkaTemplateToRegistry {
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $RegistryUrl,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $TagName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $TemplateName
)
# if registry uuid doesn't match than delete an image in registry
$images = anka --machine-readable registry --registry-path $RegistryUrl list | ConvertFrom-Json | ForEach-Object body
$images | Where-Object name -eq $TemplateName | ForEach-Object {
$id = $_.id
Show-StringWithFormat "Deleting '$TemplateName[$id]' VM and '$TagName' tag"
$uri = '{0}/registry/vm?id={1}' -f $RegistryUrl, $id
Invoke-WebRequest -Uri $uri -Method Delete | Out-Null
}
$command = "anka registry --registry-path $RegistryUrl push --force --tag $TagName $TemplateName"
Invoke-AnkaCommand -Command $command
}
function Get-AnkaVM {
param(
[string] $VMName
)
$command = "anka --machine-readable list"
if (-not [string]::IsNullOrEmpty($VMName)) {
$command = "anka --machine-readable show $VMName"
}
Invoke-AnkaCommand -Command $command | ConvertFrom-Json | Foreach-Object body
}
function Get-AnkaVMStatus {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName
)
$command = "anka --machine-readable list $VMName"
Invoke-AnkaCommand -Command $command | ConvertFrom-Json | Foreach-Object { $_.body.status }
}
function Get-AnkaVMIPAddress {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName
)
Get-AnkaVM -VMName $VMName | Foreach-Object ip
}
function Invoke-AnkaCommand {
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Command
)
$result = bash -c "$Command 2>&1" | Out-String
if ($LASTEXITCODE -ne 0) {
Write-Error "There is an error during command execution:`n$result"
exit 1
}
$result
}
function New-AnkaVMTemplate {
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $InstallerPath,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $TemplateName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $TemplateUsername,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $TemplatePassword,
[Parameter(Mandatory)]
[int] $CPUCount,
[Parameter(Mandatory)]
[int] $RamSizeGb,
[Parameter(Mandatory)]
[int] $DiskSizeGb
)
$env:ANKA_DEFAULT_USER = $TemplateUsername
$env:ANKA_DEFAULT_PASSWD = $TemplatePassword
$env:ANKA_CREATE_SUSPEND = 0
$command = "anka create --cpu-count '$CPUCount' --ram-size '${RamSizeGb}G' --disk-size '${DiskSizeGb}G' --app '$InstallerPath' $TemplateName"
Invoke-AnkaCommand -Command $command
}
function Remove-AnkaVM {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName
)
$command = "anka delete $VMName --yes"
$isTemplateExists = Get-AnkaVM | Where-Object name -eq $VMName
if ($isTemplateExists) {
$null = Invoke-AnkaCommand -Command $command
}
}
function Set-AnkaVMVideoController {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $ShortMacOSVersion,
[ValidateSet("fbuf", "pg")]
[string] $Controller = "pg"
)
$command = "anka modify $VMName set display -c $Controller"
# Apple Metal is available starting from Big Sur
if (-not $ShortMacOSVersion.StartsWith("10.")) {
$null = Invoke-AnkaCommand -Command $command
}
}
function Set-AnkaVMDisplayResolution {
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $DisplayResolution
)
$command = "anka modify $VMName set display -r $DisplayResolution"
$null = Invoke-AnkaCommand -Command $command
}
function Start-AnkaVM {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName
)
$command = "anka start $VMName"
$vmStatus = Get-AnkaVMStatus -VMName $VMName
if ($vmStatus -eq "stopped") {
$null = Invoke-AnkaCommand -Command $command
}
}
function Stop-AnkaVM {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName
)
$command = "anka stop $VMName"
$vmStatus = Get-AnkaVMStatus -VMName $VMName
if ($vmStatus -eq "running") {
$null = Invoke-AnkaCommand -Command $command
}
}
function Wait-AnkaVMIPAddress {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName,
[int] $RetryCount = 20,
[int] $Seconds = 60
)
$condition = { Get-AnkaVMIPAddress -VMName $VMName }
$null = Invoke-WithRetry -BreakCondition $condition -RetryCount $RetryCount -Seconds $Seconds
}
function Wait-AnkaVMSSHService {
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName,
[int] $RetryCount = 20,
[int] $Seconds = 60
)
Start-Sleep -Seconds $Seconds
Write-Host "`t[*] Waiting for '$VMName' VM to get an IP address"
Wait-AnkaVMIPAddress -VMName $VMName -RetryCount $RetryCount -Seconds $Seconds
$ipAddress = Get-AnkaVMIPAddress -VMName $VMName
Write-Host "`t[*] The '$ipAddress' IP address for '$VMName' VM"
Write-Host "`t[*] Checking if SSH on a port is open"
$isSSHPortOpen = Test-SSHPort -IPAddress $ipAddress
if (-not $isSSHPortOpen) {
Write-Host "`t[x] SSH port is closed"
exit 1
}
}
@@ -0,0 +1,184 @@
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[version] $MacOSVersion,
[ValidateNotNullOrEmpty()]
[string] $TemplateUsername,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $TemplatePassword,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $RegistryUrl,
[ValidateNotNullOrEmpty()]
[string] $TemplateName,
[bool] $DownloadLatestVersion = $true,
[bool] $BetaSearch = $false,
[bool] $InstallSoftwareUpdate = $true,
[bool] $EnableAutoLogon = $true,
[int] $CPUCount = 6,
[int] $RamSizeGb = 7,
[int] $DiskSizeGb = 300,
[string] $DisplayResolution = "1920x1080",
[string] $TagName = [DateTimeOffset]::Now.ToUnixTimeSeconds()
)
$ErrorActionPreference = "Stop"
$WarningPreference = "SilentlyContinue"
# Import helper modules
Import-Module "$PSScriptRoot/Anka.Helpers.psm1"
Import-Module "$PSScriptRoot/Service.Helpers.psm1"
# Helper functions
function Invoke-EnableAutoLogon {
if (-not $EnableAutoLogon) {
Write-Host "`t[*] Skip configuring AutoLogon"
return
}
$ipAddress = Get-AnkaVMIPAddress -VMName $TemplateName
Write-Host "`t[*] Enable AutoLogon"
Enable-AutoLogon -HostName $ipAddress -UserName $TemplateUsername -Password $TemplatePassword
Write-Host "`t[*] Reboot '$TemplateName' VM to enable AutoLogon"
Restart-VMSSH -HostName $ipAddress | Show-StringWithFormat
Wait-AnkaVMSSHService -VMName $TemplateName -Seconds 30
Write-Host "`t[*] Checking if AutoLogon is enabled"
Test-AutoLogon -VMName $TemplateName -UserName $TemplateUsername
}
function Invoke-SoftwareUpdate {
if (-not $InstallSoftwareUpdate) {
Write-Host "`t[*] Skip installing software updates"
return
}
$ipAddress = Get-AnkaVMIPAddress -VMName $TemplateName
# Unenroll Seed
Write-Host "`t[*] Reseting the seed before requesting stable versions"
Remove-CurrentBetaSeed -HostName $ipAddress | Show-StringWithFormat
# Install Software Updates
# Security updates may not be able to install(hang, freeze) when AutoLogon is turned off
Write-Host "`t[*] Finding available software"
$newUpdates = Get-SoftwareUpdate -HostName $ipAddress
if (-not $newUpdates) {
Write-Host "`t[*] No Updates Available"
return
}
Write-Host "`t[*] Fetching Software Updates ready to install on '$TemplateName' VM:"
Show-StringWithFormat $newUpdates
Write-Host "`t[*] Installing Software Updates on '$TemplateName' VM:"
Install-SoftwareUpdate -HostName $ipAddress | Show-StringWithFormat
# Check if Action: restart
if ($newUpdates.Contains("Action: restart")) {
Write-Host "`t[*] Sleep 60 seconds before the software updates have been installed"
Start-Sleep -Seconds 60
Write-Host "`t[*] Waiting for loginwindow process"
Wait-LoginWindow -HostName $ipAddress | Show-StringWithFormat
# Re-enable AutoLogon after installing a new security software update
Invoke-EnableAutoLogon
# Check software updates have been installed
$updates = Get-SoftwareUpdate -HostName $ipAddress
if ($updates.Contains("Action: restart")) {
Write-Host "`t[x] Software updates failed to install: $updates"
Show-StringWithFormat $updates
exit 1
}
}
Write-Host "`t[*] Show the install history:"
$hUpdates = Get-SoftwareUpdateHistory -HostName $ipAddress
Show-StringWithFormat $hUpdates
}
function Invoke-UpdateSettings {
$isConfRequired = $InstallSoftwareUpdate -or $EnableAutoLogon
if (-not $isConfRequired) {
Write-Host "`t[*] Skip additional configuration"
return
}
Write-Host "`t[*] Starting '$TemplateName' VM"
Start-AnkaVM -VMName $TemplateName
Write-Host "`t[*] Waiting for SSH service on '$TemplateName' VM"
Wait-AnkaVMSSHService -VMName $TemplateName -Seconds 30
# Configure AutoLogon
Invoke-EnableAutoLogon
# Install software updates
Invoke-SoftwareUpdate
Write-Host "`t[*] Stopping '$TemplateName' VM"
Stop-AnkaVM -VMName $TemplateName
}
function Test-VMStopped {
$vmStatus = Get-AnkaVMStatus -VMName $TemplateName
if ($vmStatus -ne "stopped") {
Write-Host "`t[x] VM '$TemplateName' state is not stopped. The current state is '$vmStatus'"
exit 1
}
}
# Password is passed as env-var "SSHPASS"
$env:SSHUSER = $TemplateUsername
$env:SSHPASS = $TemplatePassword
Write-Host "`n[#1] Download macOS application installer:"
$macOSInstaller = Get-MacOSInstaller -MacOSVersion $MacOSVersion -DownloadLatestVersion $DownloadLatestVersion -BetaSearch $BetaSearch
$shortMacOSVersion = Get-ShortMacOSVersion -MacOSVersion $MacOSVersion
if ([string]::IsNullOrEmpty($TemplateName)) {
$TemplateName = "clean_macos_${shortMacOSVersion}_${DiskSizeGb}gb"
}
Write-Host "`n[#2] Create a VM template:"
Write-Host "`t[*] Deleting existed template with name '$TemplateName' before creating a new one"
Remove-AnkaVM -VMName $TemplateName
Write-Host "`t[*] Creating Anka VM template with name '$TemplateName' and '$TemplateUsername' user"
Write-Host "`t[*] CPU Count: $CPUCount, RamSize: ${RamSizeGb}G, DiskSizeGb: ${DiskSizeGb}G, InstallerPath: $macOSInstaller, TemplateName: $TemplateName"
New-AnkaVMTemplate -InstallerPath $macOSInstaller `
-TemplateName $TemplateName `
-TemplateUsername $TemplateUsername `
-TemplatePassword $TemplatePassword `
-CPUCount $CPUCount `
-RamSizeGb $RamSizeGb `
-DiskSizeGb $DiskSizeGb | Show-StringWithFormat
Write-Host "`n[#3] Configure AutoLogon and/or install software updates:"
Invoke-UpdateSettings
Write-Host "`n[#4] Finalization '$TemplateName' configuration and push to the registry:"
Write-Host "`t[*] The '$TemplateName' VM status is stopped"
Test-VMStopped
# Configure graphics settings
Write-Host "`t[*] Enabling Graphics Acceleration with Apple Metal for '$TemplateName' VM"
Set-AnkaVMVideoController -VMName $TemplateName -ShortMacOSVersion $ShortMacOSVersion
Write-Host "`t[*] Setting screen resolution to $DisplayResolution for $TemplateName"
Set-AnkaVMDisplayResolution -VMName $TemplateName -DisplayResolution $DisplayResolution
# Push a VM template (and tag) to the Cloud
Write-Host "`t[*] Pushing '$TemplateName' image with '$TagName' tag to the '$RegistryUrl' registry..."
Push-AnkaTemplateToRegistry -RegistryUrl $registryUrl -TagName $TagName -TemplateName $TemplateName
+339
View File
@@ -0,0 +1,339 @@
function Enable-AutoLogon {
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $UserName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Password
)
$url = "https://raw.githubusercontent.com/actions/virtual-environments/main/images/macos/provision/bootstrap-provisioner/kcpassword.py"
$script = Invoke-RestMethod -Uri $url
$base64 = [Convert]::ToBase64String($script.ToCharArray())
$kcpassword = "echo $base64 | base64 --decode > ~/kcpassword;sudo python ./kcpassword '${Password}';rm ./kcpassword"
$loginwindow = "sudo /usr/bin/defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser '${UserName}';sudo /usr/bin/defaults write /Library/Preferences/com.apple.loginwindow autoLoginUserScreenLocked -bool false"
$command = "${kcpassword};$loginwindow"
Invoke-SSHPassCommand -HostName $HostName -Command $command
}
function Get-AvailableVersions {
param (
[bool] $IsBeta = $false
)
if ($IsBeta) {
$searchPostfix = " beta"
}
$command = { /usr/sbin/softwareupdate --list-full-installers | grep "macOS" }
$condition = { $LASTEXITCODE -eq 0 }
$softwareUpdates = Invoke-WithRetry -Command $command -BreakCondition $condition | Where-Object { $_.Contains("Title: macOS") -and $_ -match $searchPostfix }
$allVersions = $softwareUpdates -replace "(\* )?(Title|Version|Size):" | ConvertFrom-Csv -Header OSName, OSVersion | Select-Object OSName, OSVersion -Unique
$allVersions
}
function Get-MacOSInstaller {
param (
[Parameter(Mandatory)]
[version] $MacOSVersion,
[bool] $DownloadLatestVersion = $false,
[bool] $BetaSearch = $false
)
# Enroll machine to DeveloperSeed if we need beta and unenroll otherwise
$seedutil = "/System/Library/PrivateFrameworks/Seeding.framework/Versions/Current/Resources/seedutil"
if ($BetaSearch) {
Write-Host "`t[*] Beta Version requested. Enrolling machine to DeveloperSeed"
sudo $seedutil enroll DeveloperSeed | Out-Null
} else {
Write-Host "`t[*] Reseting the seed before requesting stable versions"
sudo $seedutil unenroll | Out-Null
}
# Validate there is no softwareupdate at the moment
Test-SoftwareUpdate
# Validate availability OSVersion
Write-Host "`t[*] Finding available full installers"
$availableVersions = Get-AvailableVersions -IsBeta $BetaSearch
if ($DownloadLatestVersion) {
$shortMacOSVersion = Get-ShortMacOSVersion -MacOSVersion $MacOSVersion
$filterSearch = "${shortMacOSVersion}."
$filteredVersions = $availableVersions.Where{ $_.OSVersion.StartsWith($filterSearch) }
if (-not $filteredVersions) {
Write-Host "`t[x] Failed to find any macOS versions using '$filterSearch' search condition"
Show-StringWithFormat $availableVersions
exit 1
}
Show-StringWithFormat $filteredVersions
$osVersions = $filteredVersions.OSVersion | Sort-Object {[version]$_}
$MacOSVersion = $osVersions | Select-Object -Last 1
Write-Host "`t[*] The 'DownloadLatestVersion' flag is set. Latest macOS version is '$MacOSVersion' now"
}
$macOSName = $availableVersions.Where{ $MacOSVersion -eq $_.OSVersion }.OSName
if (-not $macOSName) {
Write-Host "`t[x] Requested macOS '$MacOSVersion' version not found in the list of available installers. Available versions are:`n$($availableVersions.OSVersion)"
Write-Host "`t[x] Make sure to pass '-BetaSearch `$true' if you need a beta version installer"
exit 1
}
$installerPathPattern = "/Applications/Install*${macOSName}.app"
if (Test-Path $installerPathPattern) {
$previousInstallerPath = Get-Item -Path $installerPathPattern
Write-Host "`t[*] Removing '$previousInstallerPath' installation app before downloading the new one"
sudo rm -rf "$previousInstallerPath"
}
# Clear LastRecommendedMajorOSBundleIdentifier to prevent error during fetching updates
# Install failed with error: Update not found
Update-SoftwareBundle
# Download macOS installer
Write-Host "`t[*] Requested macOS '$MacOSVersion' version installer found, fetching it from Apple Software Update"
$result = Invoke-WithRetry { /usr/sbin/softwareupdate --fetch-full-installer --full-installer-version $MacOSVersion } {$LASTEXITCODE -eq 0} | Out-String
if (-not $result.Contains("Install finished successfully")) {
Write-Host "`t[x] Failed to fetch $MacOSVersion macOS `n$result"
exit 1
}
$installerPath = (Get-Item -Path $installerPathPattern).FullName
Write-Host "`t[*] Installer successfully downloaded to '$installerPath'"
$installerPath
}
function Get-ShortMacOSVersion {
param (
[Parameter(Mandatory)]
[version] $MacOSVersion
)
# Take Major.Minor version for macOS 10 (10.14 or 10.15) and Major for all further versions
$MacOSVersion.Major -eq 10 ? $MacOSVersion.ToString(2) : $MacOSVersion.ToString(1)
}
function Get-SoftwareUpdate {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName
)
$command = "/usr/sbin/softwareupdate --list"
$result = Invoke-SSHPassCommand -HostName $HostName -Command $command
$result | Where-Object { $_ -match "(Label|Title):" } | Out-String
}
function Get-SoftwareUpdateHistory {
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName
)
$command = "/usr/sbin/softwareupdate --history"
Invoke-SSHPassCommand -HostName $HostName -Command $command | Out-String
}
function Install-SoftwareUpdate {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName
)
$command = "sudo /usr/sbin/softwareupdate --all --install --restart --verbose"
Invoke-SSHPassCommand -HostName $HostName -Command $command
}
function Invoke-SSHPassCommand {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Command,
[int] $ConnectTimeout = 10,
[int] $ConnectionAttempts = 10,
[int] $ServerAliveInterval = 30
)
$sshArg = @(
"sshpass"
"-e"
"ssh"
"-o UserKnownHostsFile=/dev/null"
"-o StrictHostKeyChecking=no"
"-o ConnectTimeout=$ConnectTimeout"
"-o ConnectionAttempts=$ConnectionAttempts"
"-o LogLevel=ERROR"
"-o ServerAliveInterval=$ServerAliveInterval"
"${env:SSHUSER}@${HostName}"
)
$sshPassOptions = $sshArg -join " "
bash -c "$sshPassOptions \""$Command\"" 2>&1"
}
function Invoke-WithRetry {
param(
[scriptblock] $Command,
[scriptblock] $BreakCondition,
[int] $RetryCount = 20,
[int] $Seconds = 60
)
while ($RetryCount -gt 0) {
if ($Command) {
$result = & $Command
}
if (& $BreakCondition) {
return $result
}
$RetryCount--
if ($RetryCount -eq 0) {
Write-Error "No more attempts left: $BreakCondition"
}
Write-Host "`t [/] Waiting $Seconds seconds before retrying. Retries left: $RetryCount"
Start-Sleep -Seconds $Seconds
}
$result
}
function Restart-VMSSH {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName
)
$command = "sudo reboot"
Invoke-SSHPassCommand -HostName $HostName -Command $command
}
function Show-StringWithFormat {
param(
[Parameter(ValuefromPipeline)]
[object] $string
)
process {
($string | Out-String).Trim().split("`n") | ForEach-Object { Write-Host "`t $_" }
}
}
function Remove-CurrentBetaSeed {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName
)
$command = "sudo /System/Library/PrivateFrameworks/Seeding.framework/Versions/Current/Resources/seedutil unenroll"
Invoke-SSHPassCommand -HostName $HostName -Command $command | Out-String
}
function Test-AutoLogon {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $VMName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $UserName
)
Invoke-WithRetry -BreakCondition {
# pwsh crashes if it invokes directly
# https://github.com/dotnet/runtime/issues/59059
$ankaUser = "" | bash -c "anka run $VMName /usr/bin/id -nu"
$UserName -eq $ankaUser
}
}
function Test-SoftwareUpdate {
param (
[string] $UpdateProcessName = "softwareupdate"
)
$command = {
$updateProcess = (Get-Process -Name $UpdateProcessName -ErrorAction SilentlyContinue).id
if ($updateProcess) {
# Workaround to get commandline param as it doesn't work for macOS atm https://github.com/PowerShell/PowerShell/issues/13943
$processName = /bin/ps -o command= $updateProcess
Write-Host "`t[*] Another software update process with '$updateProcess' id is in place with the following arguments '$processName'"
}
}
$condition = {
$null -eq (Get-Process -Name $UpdateProcessName -ErrorAction SilentlyContinue)
}
Invoke-WithRetry -Command $command -BreakCondition $condition
}
function Test-SSHPort {
param(
[Parameter(Mandatory)]
[ipaddress] $IPAddress,
[int] $Port = 22,
[int] $Timeout = 2000
)
Invoke-WithRetry -Command {$true} -BreakCondition {
try {
$client = [System.Net.Sockets.TcpClient]::new()
$client.ConnectAsync($IPAddress, $Port).Wait($Timeout)
}
catch {
$false
}
finally {
$client.Close()
}
}
}
function Wait-LoginWindow {
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $HostName,
[int] $RetryCount = 60,
[int] $Seconds = 60
)
$condition = {
$psCommand = "/bin/ps auxww"
$lw = "/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow"
$ctk = "/System/Library/Frameworks/CryptoTokenKit.framework/ctkahp.bundle/Contents/MacOS/ctkahp"
$proc = Invoke-SSHPassCommand -HostName $HostName -Command $psCommand | Out-String
$proc.Contains($lw) -and $proc.Contains($ctk)
}
Invoke-WithRetry -RetryCount $RetryCount -Seconds $Seconds -BreakCondition $condition
}
function Update-SoftwareBundle {
$productVersion = sw_vers -productVersion
if ( $productVersion.StartsWith('11.') ) {
sudo rm -rf /Library/Preferences/com.apple.commerce.plist
sudo /usr/bin/defaults delete /Library/Preferences/com.apple.SoftwareUpdate.plist LastRecommendedMajorOSBundleIdentifier | Out-Null
}
}
@@ -0,0 +1,149 @@
jobs:
- job: Image_generation
displayName: Image Generation (${{ parameters.image_label }})
timeoutInMinutes: 720
pool:
name: Mac-Cloud Image Generation
variables:
- group: Mac-Cloud Image Generation
- group: Mac-Cloud Image Generation Key Vault
steps:
- pwsh: |
$cleanBuildNumber = "$(Build.BuildNumber)" -replace "(.+_unstable)(\.\d+)", '$1'
$virtualMachineName = "${cleanBuildNumber}.$(System.JobAttempt)"
echo "##vso[task.setvariable variable=VirtualMachineName;]$virtualMachineName"
echo "##vso[build.updatebuildnumber]$virtualMachineName"
displayName: Update BuildNumber
- checkout: self
clean: true
fetchDepth: 1
- task: PowerShell@2
displayName: 'Validate contributor permissions'
condition: startsWith(variables['Build.SourceBranch'], 'refs/pull/')
inputs:
targetType: 'filePath'
filePath: ./images.CI/macos/validate-contributor.ps1
pwsh: true
arguments: -RepositoryName "$(Build.Repository.Name)" `
-AccessToken "$(github-feed-token)" `
-SourceBranch "$(Build.SourceBranch)" `
-ContributorAllowList "$(CONTRIBUTOR_ALLOWLIST)"
- task: PowerShell@2
displayName: 'Download custom repository'
condition: and(ne(variables['CUSTOM_REPOSITORY_URL'], ''), ne(variables['CUSTOM_REPOSITORY_BRANCH'], ''))
inputs:
targetType: 'filePath'
filePath: ./images.CI/download-repo.ps1
arguments: -RepoUrl $(CUSTOM_REPOSITORY_URL) `
-RepoBranch $(CUSTOM_REPOSITORY_BRANCH)
- task: DeleteFiles@1
displayName: Clean up self-hosted machine
inputs:
SourceFolder: 'images/macos/provision/log/'
RemoveSourceFolder: true
- task: PowerShell@2
displayName: 'Select datastore'
inputs:
targetType: 'filePath'
filePath: ./images.CI/macos/select-datastore.ps1
arguments: -VMName "$(VirtualMachineName)" `
-VIServer "$(vcenter-server-v2)" `
-VIUserName "$(vcenter-username-v2)" `
-VIPassword '$(vcenter-password-v2)' `
-Cluster "$(esxi-cluster-v2)"
- pwsh: |
$SensitiveData = @(
'IP address:',
'Using ssh communicator to connect:'
)
packer build -on-error=abort `
-var="vcenter_server=$(vcenter-server-v2)" `
-var="vcenter_username=$(vcenter-username-v2)" `
-var='vcenter_password=$(vcenter-password-v2)' `
-var="vcenter_datacenter=$(vcenter-datacenter-v2)" `
-var="cluster_or_esxi_host=$(esxi-cluster-v2)" `
-var="esxi_datastore=$(buildDatastore)" `
-var="output_folder=$(output-folder)" `
-var="vm_username=$(vm-username)" `
-var="vm_password=$(vm-password)" `
-var="github_api_pat=$(github_api_pat)" `
-var="build_id=$(VirtualMachineName)" `
-var="baseimage_name=${{ parameters.base_image_name }}" `
-var="xcode_install_user=$(xcode-installation-user)" `
-var="xcode_install_password=$(xcode-installation-password)" `
-color=false `
${{ parameters.template_path }} `
| Where-Object {
#Filter sensitive data from Packer logs
$currentString = $_
$sensitiveString = $SensitiveData | Where-Object { $currentString -match $_ }
$sensitiveString -eq $null
}
displayName: 'Build VM'
workingDirectory: 'images/macos'
env:
PACKER_LOG: 1
PACKER_LOG_PATH: $(Agent.TempDirectory)/packer-log.txt
- bash: |
echo "Copy image output files"
cp -R "images/image-output/software-report/." "$(Build.ArtifactStagingDirectory)/"
echo "Copy test results"
cp -R "images/image-output/tests/." "$(Common.TestResultsDirectory)/"
ls $(Common.TestResultsDirectory)
echo "Put VM name to 'VM_Done_Name' file"
echo "$(VirtualMachineName)" > "$(Build.ArtifactStagingDirectory)/VM_Done_Name"
displayName: Prepare artifact
- bash: |
cat "$(Build.ArtifactStagingDirectory)/systeminfo.md"
displayName: Print software report
- task: PublishBuildArtifacts@1
inputs:
ArtifactName: 'Built_VM_Artifacts'
displayName: Publish Artifacts
- task: PowerShell@2
displayName: 'Print provisioners duration'
inputs:
targetType: 'filePath'
filePath: ./images.CI/measure-provisioners-duration.ps1
arguments: -PackerLogPath "$(Agent.TempDirectory)/packer-log.txt" `
-PrintTopNLongest 25
- task: PowerShell@2
displayName: 'Move vm to cold storage and clear datastore tag'
condition: always()
inputs:
targetType: 'filePath'
filePath: ./images.CI/macos/move-vm.ps1
arguments: -VMName "$(VirtualMachineName)" `
-TargetDataStore "${{ parameters.target_datastore }}" `
-VIServer "$(vcenter-server-v2)" `
-VIUserName "$(vcenter-username-v2)" `
-VIPassword '$(vcenter-password-v2)' `
-CpuCount "$(cpu-count)" `
-CoresPerSocketCount "$(cores-per-socket-count)" `
-Memory "$(memory)"
- task: PowerShell@2
displayName: 'Destroy VM (if build canceled only)'
condition: eq(variables['Agent.JobStatus'], 'Canceled')
inputs:
targetType: 'filePath'
filePath: ./images.CI/macos/destroy-vm.ps1
arguments: -VMName "$(VirtualMachineName)" `
-VIServer "$(vcenter-server-v2)" `
-VIUserName "$(vcenter-username-v2)" `
-VIPassword '$(vcenter-password-v2)'
@@ -0,0 +1,23 @@
name: macOS-10.15_$(date:yyyyMMdd)$(rev:.r)_unstable
schedules:
- cron: "0 0 * * *"
displayName: Daily
branches:
include:
- main
always: true
trigger: none
pr:
autoCancel: true
branches:
include:
- main
jobs:
- template: image-generation.yml
parameters:
image_label: 'macOS Catalina'
base_image_name: 'clean-macOS-10.15-380Gb-runner'
template_path: 'templates/macOS-10.15.json'
target_datastore: 'ds-image'
@@ -0,0 +1,23 @@
name: macOS-11_$(date:yyyyMMdd)$(rev:.r)_unstable
schedules:
- cron: "0 0 * * *"
displayName: Daily
branches:
include:
- main
always: true
trigger: none
pr:
autoCancel: true
branches:
include:
- main
jobs:
- template: image-generation.yml
parameters:
image_label: 'macOS Big Sur'
base_image_name: 'clean-macOS-11-380Gb-runner'
template_path: 'templates/macOS-11.json'
target_datastore: 'ds-image'
@@ -0,0 +1,23 @@
name: macOS-12_$(date:yyyyMMdd)$(rev:.r)_unstable
schedules:
- cron: "0 0 * * *"
displayName: Daily
branches:
include:
- main
always: true
trigger: none
pr:
autoCancel: true
branches:
include:
- main
jobs:
- template: image-generation.yml
parameters:
image_label: 'macOS Monterey'
base_image_name: 'clean-macOS-12-380Gb-runner'
template_path: 'templates/macOS-12.json'
target_datastore: 'ds-image'
+95
View File
@@ -0,0 +1,95 @@
<#
.SYNOPSIS
This script deletes vm from vCenter
.PARAMETER VMName
VM name to delete (Example "macOS-10.15_20201012.4")
.PARAMETER VIServer
vCenter address (Example "10.0.1.16")
.PARAMETER VIUserName
vCenter username (Example "Administrator")
.PARAMETER VIPassword
vCenter password (Example "12345678")
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VMName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIServer,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIUserName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIPassword
)
# Import helpers module
Import-Module $PSScriptRoot\helpers.psm1 -DisableNameChecking
# Connection to a vCenter Server system
Connect-VCServer -VIServer $VIServer -VIUserName $VIUserName -VIPassword $VIPassword
# Check vm clone status
$chainId = (Get-VIEvent -Entity $VMName).ChainId
if ($chainId)
{
$task = Get-Task -Status Running | Where-Object { ($_.Name -eq 'CloneVM_Task') -and ($_.ExtensionData.Info.EventChainId -in $chainId) }
if ($task)
{
try
{
Stop-Task -Task $task -Confirm:$false -ErrorAction Stop
Write-Host "The vm '$VMName' clone task has been canceled"
}
catch
{
Write-Host "##vso[task.LogIssue type=error;]Failed to cancel the task"
}
}
}
# Remove a vm
$vm = Get-VM -Name $VMName -ErrorAction SilentlyContinue
if ($vm)
{
$vmState = $vm.PowerState
if ($vmState -ne "PoweredOff")
{
try
{
$null = Stop-VM -VM $vm -Confirm:$false -ErrorAction Stop
Write-Host "The vm '$VMName' has been powered off"
}
catch
{
Write-Host "##vso[task.LogIssue type=error;]Failed to shutdown '$VMName'"
}
}
try
{
Remove-VM -VM $vm -DeletePermanently -Confirm:$false -ErrorAction Stop
Write-Host "The vm '$VMName' has been removed"
}
catch
{
Write-Host "##vso[task.LogIssue type=error;]Failed to remove '$VMName'"
}
}
else
{
Write-Host "VM '$VMName' not found"
}
+38
View File
@@ -0,0 +1,38 @@
<#
.SYNOPSIS
Helper functions to use in images.CI scripts
#>
Function Connect-VCServer
{
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.String]$VIUserName,
[Parameter(Mandatory)]
[System.String]$VIPassword,
[Parameter(Mandatory)]
[System.String]$VIServer
)
try
{
# Preference
$global:ProgressPreference = 'SilentlyContinue'
$global:WarningPreference = 'SilentlyContinue'
# Ignore SSL
$null = Set-PowerCLIConfiguration -Scope Session -InvalidCertificateAction Ignore -ParticipateInCEIP $false -Confirm:$false -WebOperationTimeoutSeconds 600
$securePassword = ConvertTo-SecureString -String $VIPassword -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential($VIUserName, $securePassword)
$null = Connect-VIServer -Server $VIServer -Credential $cred -ErrorAction Stop
Write-Host "Connection to the vSphere server has been established"
}
catch
{
Write-Host "##vso[task.LogIssue type=error;]Failed to connect to the vSphere server"
exit 1
}
}
+92
View File
@@ -0,0 +1,92 @@
<#
.SYNOPSIS
This script migrates given VM to another datastore
.PARAMETER VMName
VM name to migrate (Example "macOS-10.15_20201012.4")
.PARAMETER TargetDataStore
Target datastore (Example "ds-image")
.PARAMETER VIServer
vCenter address (Example "10.0.1.16")
.PARAMETER VIUserName
vCenter username (Example "Administrator")
.PARAMETER VIPassword
vCenter password (Example "12345678")
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VMName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$TargetDataStore,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIServer,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIUserName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIPassword,
[int32]$CpuCount,
[int32]$CoresPerSocketCount,
[int64]$Memory
)
# Import helpers module
Import-Module $PSScriptRoot\helpers.psm1 -DisableNameChecking
# Connection to a vCenter Server system
Connect-VCServer -VIServer $VIServer -VIUserName $VIUserName -VIPassword $VIPassword
# Clear previously assigned tag with VM Name
try {
Remove-Tag $VMName -Confirm:$false
} catch {
Write-Host "Tag with $VMName doesn't exist"
}
$vm = Get-VM $VMName
if ($env:AGENT_JOBSTATUS -eq 'Failed') {
try {
if($vm.PowerState -ne "PoweredOff") {
Stop-VM -VM $vm -Confirm:$false -ErrorAction Stop | Out-Null
}
Set-VM -VM $vm -Name "${VMName}_failed" -Confirm:$false -ErrorAction Stop | Out-Null
Write-Host "VM has been successfully powered off and renamed to [${VMName}_failed]"
} catch {
Write-Host "##vso[task.LogIssue type=error;]Failed to power off and rename VM '$VMName'"
exit 1
}
}
try {
Move-VM -Vm $vm -Datastore $TargetDataStore -ErrorAction Stop | Out-Null
Write-Host "VM has been moved successfully to target datastore '$TargetDataStore'"
} catch {
Write-Host "##vso[task.LogIssue type=error;]Failed to move VM '$VMName' to target datastore '$TargetDataStore'"
exit 1
}
try {
Write-Host "Change CPU count to $CpuCount, cores count to $CoresPerSocketCount, amount of RAM to $Memory"
$vm | Set-VM -NumCPU $CpuCount -CoresPerSocket $CoresPerSocketCount -MemoryMB $Memory -Confirm:$false -ErrorAction Stop | Out-Null
} catch {
Write-Host "##vso[task.LogIssue type=error;]Failed to change specs for VM '$VMName'"
exit 1
}
+115
View File
@@ -0,0 +1,115 @@
<#
.SYNOPSIS
This script selects local datastore based on the following rules:
- Name starts with ds-local-Datastore
- Datastore FreespaceGB > 400 Gb
- VM count on the datastore < 2
.PARAMETER VIServer
vCenter address (Example "10.0.1.16")
.PARAMETER VIUserName
vCenter username (Example "Administrator")
.PARAMETER VIPassword
vCenter password (Example "12345678")
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VMName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIServer,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIUserName,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$VIPassword,
[string]$TagCategory = "Busy",
[string]$Cluster
)
# Import helpers module
Import-Module $PSScriptRoot\helpers.psm1 -DisableNameChecking
function Select-DataStore {
param (
[string]$VMName,
[string]$TagCategory,
[string]$TemplateDatastore = "ds-local-Datastore-*",
[string]$Cluster,
[int]$ThresholdInGb = 400,
[int]$VMCount = 2,
[int]$Retries = 5
)
# 1. Name starts with ds-local-Datastore
# 2. FreespaceGB > 400 Gb
# 3. Choose a datastore with the minimal VM count < 2
Write-Host "Start Datastore selection process..."
$clusterHosts = Get-Cluster -Name $Cluster | Get-VMHost
$availableClusterDatastores = $clusterHosts | Get-Datastore -Name $TemplateDatastore | Where-Object -Property State -eq "Available"
$availableDatastores = $availableClusterDatastores `
| Where-Object { $_.FreeSpaceGB -ge $thresholdInGb } `
| Where-Object {
$vmOnDatastore = @((Get-ChildItem -Path $_.DatastoreBrowserPath).Name -notmatch "^\.").Count
$vmOnDatastore -lt $vmCount } `
| Group-Object -Property { $vmOnDatastore }
$datastore = $availableDatastores | Select-Object @{n="VmCount";e={$_.Name}},@{n="DatastoreName";e={$_.Group | Get-Random}} -First 1
$buildDatastore = $datastore.DatastoreName
$tag = Get-Tag -Category $TagCategory -Name $VMName -ErrorAction Ignore
if (-not $tag)
{
$tag = New-Tag -Name $VMName -Category $TagCategory
}
New-TagAssignment -Tag $tag -Entity $buildDatastore | Out-Null
# Wait for 60 seconds to check if any other tags are assigned to the same datastore
Start-Sleep -Seconds 60
# If there are no datastores with 0 VMs, take a datastore with 1 VM (index 1 if datastore has 0 VMs and 2 if 1 VM)
$index = 1 + [int]$datastore.VmCount
$tagAssignments = (Get-TagAssignment -Entity $buildDatastore).Tag.Name | Select-Object -First $index
$isAllow = $tagAssignments -contains $VMName
if ($isAllow)
{
Write-Host "Datastore selected successfully"
Write-Host "##vso[task.setvariable variable=buildDatastore;issecret=true]$buildDatastore"
return
}
# Remove the tag if datastore wasn't selected
Remove-Tag $tag -Confirm:$false
$retries--
if ($retries -le 0)
{
Write-Host "##vso[task.LogIssue type=error;]No datastores found for the condition"
exit 1
}
Write-Host "Datastore select failed, $retries left"
Select-DataStore -VMName $VMName -Cluster $Cluster -TagCategory $TagCategory -Retries $retries
}
# Connection to a vCenter Server system
Connect-VCServer -VIServer $VIServer -VIUserName $VIUserName -VIPassword $VIPassword
# Get a target datastore for current deployment
Select-DataStore -VMName $VMName -Cluster $Cluster -TagCategory $TagCategory
+63
View File
@@ -0,0 +1,63 @@
param(
[Parameter(Mandatory)] [string] $RepositoryName,
[Parameter(Mandatory)] [string] $AccessToken,
[Parameter(Mandatory)] [string] $SourceBranch,
[Parameter(Mandatory)] [string] $ContributorAllowList
)
function Build-AuthHeader {
param(
[Parameter(Mandatory)] [string] $AccessToken
)
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("'':${AccessToken}"))
return "Basic ${base64AuthInfo}"
}
function Get-PullRequest {
param(
[Parameter(Mandatory)] [string] $RepositoryName,
[Parameter(Mandatory)] [string] $AccessToken,
[Parameter(Mandatory)] [UInt32] $PullRequestNumber
)
$requestUrl = "https://api.github.com/repos/$RepositoryName/pulls/$PullRequestNumber"
$authHeader = Build-AuthHeader -AccessToken $AccessToken
$params = @{
Method = "GET"
ContentType = "application/json"
Uri = $requestUrl
Headers = @{ Authorization = $authHeader }
}
return Invoke-RestMethod @params
}
function Validate-ContributorPermissions {
param(
[Parameter(Mandatory)] [string] $ContributorAllowList,
[Parameter(Mandatory)] [string] $ContributorName
)
$allowedContributors = $ContributorAllowList.Split(",").Trim()
$validСontributor = $allowedContributors | Where-Object { $_ -eq $ContributorName } `
| Select-Object -First 1
if (-not $validСontributor) {
Write-Host "Failed to start this build. '$ContributorName' is an unknown contributor"
Write-Host "Please add '$ContributorName' to the allowed list to run builds"
exit 1
}
}
$pullRequestNumber = $SourceBranch.Split("/")[2]
$pullRequestInfo = Get-PullRequest -RepositoryName $RepositoryName `
-AccessToken $AccessToken `
-PullRequestNumber $pullRequestNumber
$contributorName = $pullRequestInfo.user.login
Validate-ContributorPermissions -ContributorAllowList $ContributorAllowList `
-ContributorName $contributorName
+4 -4
View File
@@ -22,8 +22,8 @@ function Validate-Scripts {
return $ScriptWithoutShebangLine
}
$PathUbuntu = "./images/ubuntu/scripts"
$PathMacOS = "./images/macos"
$PathUbuntu = "./images/linux/scripts"
$PathMacOS = "./images/macos/provision"
$PatternUbuntu = "#!/bin/bash -e"
$PatternMacOS = "#!/bin/bash -e -o pipefail"
$ScriptsWithBrokenShebang = @()
@@ -34,10 +34,10 @@ if ($ScriptsWithBrokenShebang.Length -gt 0) {
$ScriptsWithBrokenShebang | ForEach-Object {
Write-Host "##[error] '$_'"
}
Write-Host "`n`n##[error] Expected shebang for scripts in 'images/ubuntu' folder is '$PatternUbuntu'"
Write-Host "`n`n##[error] Expected shebang for scripts in 'images/linux' folder is '$PatternUbuntu'"
Write-Host "##[error] Expected shebang for scripts in 'images/macos' folder is '$PatternMacOS'"
exit 1
else {
Write-Host "All scripts have correct shebang."
}
}
}
+374
View File
@@ -0,0 +1,374 @@
| Announcements |
|-|
| [[All OSs] .NET 2.1 will be removed from the images on February, 21](https://github.com/actions/virtual-environments/issues/4871) |
| [[Ubuntu] Issue with libstdc++ cannot allocate memory in static TLS block](https://github.com/actions/virtual-environments/issues/4799) |
***
# Ubuntu 18.04.6 LTS
- Linux kernel version: 5.4.0-1069-azure
- Image Version: 20220213.1
## Installed Software
### Language and Runtime
- Bash 4.4.20(1)-release
- Clang 9.0.0
- Clang-format 9.0.0
- Erlang 24.2.1 (Eshell 12.2.1)
- Erlang rebar3 3.18.0
- GNU C++ 7.5.0, 9.4.0, 10.3.0
- GNU Fortran 7.5.0, 9.4.0, 10.3.0
- Julia 1.7.2
- Kotlin 1.6.10-release-923
- Mono 6.12.0.122 (apt source repository: https://download.mono-project.com/repo/ubuntu stable-bionic main)
- MSBuild 16.6.0.15201 (from /usr/lib/mono/msbuild/15.0/bin/MSBuild.dll)
- Node 16.14.0
- Perl 5.26.1
- Python 2.7.17
- Python3 3.6.9
- Ruby 2.5.1p57
- Swift 5.5.3
### Package Management
- cpan 1.64
- Helm 3.8.0
- Homebrew 3.3.14
- Miniconda 4.10.3
- Npm 8.3.1
- Pip 9.0.1
- Pip3 9.0.1
- Pipx 1.0.0
- RubyGems 2.7.6
- Vcpkg (build from master \<8dbd66f5a>)
- Yarn 1.22.17
#### Environment variables
| Name | Value |
| ----------------------- | ---------------------- |
| CONDA | /usr/share/miniconda |
| VCPKG_INSTALLATION_ROOT | /usr/local/share/vcpkg |
### Project Management
- Ant 1.10.5
- Gradle 7.4
- Maven 3.8.4
- Sbt 1.6.2
### Tools
- Ansible 2.11.8
- apt-fast 1.9.12
- AzCopy 10.13.0 (available by `azcopy` and `azcopy10` aliases)
- Bazel 5.0.0
- Bazelisk 1.11.0
- Bicep 0.4.1272
- Buildah 1.19.6 (apt source repository: https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable)
- CMake 3.22.2
- CodeQL Action Bundle 2.8.0
- Docker Compose v1 1.29.2
- Docker Compose v2 2.2.3+azure-1
- Docker-Buildx 0.7.1
- Docker-Moby Client 20.10.11+azure-3
- Docker-Moby Server 20.10.11+azure-3
- Git 2.35.1 (apt source repository: ppa:git-core/ppa)
- Git LFS 3.0.2 (apt source repository: https://packagecloud.io/install/repositories/github/git-lfs)
- Git-ftp 1.3.1
- Haveged 1.9.1
- Heroku 7.59.2
- HHVM (HipHop VM) 4.148.0
- jq 1.5
- Kind 0.11.1
- Kubectl 1.23.3
- Kustomize 4.5.2
- Leiningen 2.9.8
- MediaInfo 17.12
- Mercurial 4.5.3
- Minikube 1.25.1
- n 8.0.2
- Newman 5.3.1
- nvm 0.39.1
- OpenSSL 1.1.1 11 Sep 2018
- Packer 1.7.10
- Parcel 2.3.1
- PhantomJS 2.1.1
- Podman 3.0.1 (apt source repository: https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable)
- Pulumi 3.24.1
- R 4.1.2
- Skopeo 1.2.2 (apt source repository: https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable)
- Sphinx Open Source Search Server 2.2.11
- SVN 1.9.7
- Terraform 1.1.5
- yamllint 1.26.3
- yq 4.19.1
- zstd 1.5.2 (homebrew)
### CLI Tools
- Alibaba Cloud CLI 3.0.107
- AWS CLI 1.22.54
- AWS CLI Session manager plugin 1.2.295.0
- AWS SAM CLI 1.37.0
- Azure CLI (azure-cli) 2.33.0 (installation method: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt)
- Azure CLI (azure-devops) 0.23.0
- GitHub CLI 2.5.0
- Google Cloud SDK 369.0.0 (apt source repository: https://packages.cloud.google.com/apt)
- Hub CLI 2.14.2
- Netlify CLI 9.4.0
- OpenShift CLI 4.9.19
- ORAS CLI 0.12.0
- Vercel CLI 23.1.2
### Java
| Version | Vendor | Environment Variable |
| ------------------- | --------------- | -------------------- |
| 8.0.322+6 (default) | Eclipse Temurin | JAVA_HOME_8_X64 |
| 11.0.14.1+1 | Eclipse Temurin | JAVA_HOME_11_X64 |
| 12.0.2+10 | Adopt OpenJDK | JAVA_HOME_12_X64 |
| 17.0.2+8 | Eclipse Temurin | JAVA_HOME_17_X64 |
### PHP
| Tool | Version |
| -------- | ---------------------------------------- |
| PHP | 7.1.33 7.2.34 7.3.33 7.4.27 8.0.15 8.1.2 |
| Composer | 2.2.6 |
| PHPUnit | 8.5.23 |
```
Both Xdebug and PCOV extensions are installed, but only Xdebug is enabled.
```
### Haskell
- Cabal 3.6.2.0
- GHC 9.2.1
- GHCup 0.1.17.4
- Stack 2.7.3
### Rust Tools
- Cargo 1.58.0
- Rust 1.58.1
- Rustdoc 1.58.1
- Rustup 1.24.3
#### Packages
- Bindgen 0.59.2
- Cargo audit 0.16.0
- Cargo clippy 0.1.58
- Cargo outdated 0.10.2
- Cbindgen 0.20.0
- Rustfmt 1.4.38
### Browsers and Drivers
- Google Chrome 98.0.4758.80
- ChromeDriver 98.0.4758.80
- Mozilla Firefox 96.0
- Geckodriver 0.30.0
- Chromium 98.0.4758.0
- Selenium server 4.1.0
#### Environment variables
| Name | Value |
| ----------------- | ----------------------------------- |
| CHROMEWEBDRIVER | /usr/local/share/chrome_driver |
| GECKOWEBDRIVER | /usr/local/share/gecko_driver |
| SELENIUM_JAR_PATH | /usr/share/java/selenium-server.jar |
### .NET Core SDK
- 2.1.302 2.1.403 2.1.526 2.1.617 2.1.701 2.1.818 3.1.120 3.1.202 3.1.302 3.1.416 5.0.104 5.0.211 5.0.303 5.0.405 6.0.102
### .NET tools
- nbgv 3.4.255+06fb9182bf
### Databases
- MongoDB 5.0.6 (apt source repository: https://repo.mongodb.org/apt/ubuntu)
- PostgreSQL 14.2 (apt source repository: https://apt.postgresql.org/pub/repos/apt/)
- sqlite3 3.22.0
#### MySQL
- MySQL 5.7.37
- MySQL Server (user:root password:root)
```
MySQL service is disabled by default. Use the following command as a part of your job to start the service: 'sudo systemctl start mysql.service'
```
#### MS SQL Server Client Tools
- sqlcmd 17.8.0001.1
- SqlPackage 16.0.5400.1
### Cached Tools
#### Go
- 1.15.15
- 1.16.14
- 1.17.7
#### Node.js
- 12.22.10
- 14.19.0
- 16.14.0
#### PyPy
- 2.7.18 [PyPy 7.3.6]
- 3.6.12 [PyPy 7.3.3]
#### Python
- 2.7.18
- 3.6.15
- 3.7.12
- 3.8.12
- 3.9.10
- 3.10.2
#### Ruby
- 2.4.10
- 2.5.9
- 2.6.9
- 2.7.5
- 3.0.3
#### Environment variables
| Name | Value | Architecture |
| --------------- | ----------------------------------- | ------------ |
| GOROOT_1_15_X64 | /opt/hostedtoolcache/go/1.15.15/x64 | x64 |
| GOROOT_1_16_X64 | /opt/hostedtoolcache/go/1.16.14/x64 | x64 |
| GOROOT_1_17_X64 | /opt/hostedtoolcache/go/1.17.7/x64 | x64 |
### PowerShell Tools
- PowerShell 7.2.1
#### PowerShell Modules
| Module | Version |
| ---------- | ------- |
| MarkdownPS | 1.9 |
| Pester | 5.3.1 |
#### Az PowerShell Modules
- 7.1.0 3.1.0.zip 4.4.0.zip 5.9.0.zip 6.6.0.zip
### Web Servers
| Name | Version | ConfigFile | ServiceStatus | ListenPort |
| ------- | ------- | ------------------------- | ------------- | ---------- |
| apache2 | 2.4.29 | /etc/apache2/apache2.conf | inactive | 80 |
| nginx | 1.14.0 | /etc/nginx/nginx.conf | inactive | 80 |
### Android
| Package Name | Version |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Android Command Line Tools | 4.0 |
| Android Emulator | 31.2.8 |
| Android SDK Build-tools | 32.0.0<br>31.0.0<br>30.0.0 30.0.1 30.0.2 30.0.3<br>29.0.0 29.0.1 29.0.2 29.0.3<br>28.0.0 28.0.1 28.0.2 28.0.3<br>27.0.0 27.0.1 27.0.2 27.0.3<br>26.0.0 26.0.1 26.0.2 26.0.3<br>25.0.0 25.0.1 25.0.2 25.0.3<br>24.0.0 24.0.1 24.0.2 24.0.3<br>23.0.1 23.0.2 23.0.3 |
| Android SDK Platform-Tools | 32.0.0 |
| Android SDK Platforms | android-32 (rev 1)<br>android-31 (rev 1)<br>android-30 (rev 3)<br>android-29 (rev 5)<br>android-28 (rev 6)<br>android-27 (rev 3)<br>android-26 (rev 2)<br>android-25 (rev 3)<br>android-24 (rev 2)<br>android-23 (rev 3) |
| Android SDK Tools | 26.1.1 |
| Android Support Repository | 47.0.0 |
| CMake | 3.10.2<br>3.18.1 |
| Google APIs | addon-google_apis-google-21<br>addon-google_apis-google-22<br>addon-google_apis-google-23<br>addon-google_apis-google-24 |
| Google Play services | 49 |
| Google Repository | 58 |
| NDK | 21.4.7075529 (default)<br>23.1.7779620 |
| SDK Patch Applier v4 | 1 |
#### Environment variables
| Name | Value |
| ----------------------- | ------------------------------------------------------------------------------------ |
| ANDROID_HOME | /usr/local/lib/android/sdk |
| ANDROID_NDK_HOME | /usr/local/lib/android/sdk/ndk-bundle -> /usr/local/lib/android/sdk/ndk/21.4.7075529 |
| ANDROID_NDK_LATEST_HOME | /usr/local/lib/android/sdk/ndk/23.1.7779620 |
| ANDROID_NDK_ROOT | /usr/local/lib/android/sdk/ndk-bundle -> /usr/local/lib/android/sdk/ndk/21.4.7075529 |
| ANDROID_SDK_ROOT | /usr/local/lib/android/sdk |
### Cached Docker images
| Repository:Tag | Digest | Created |
| ----------------------- | ------------------------------------------------------------------------ | ---------- |
| alpine:3.12 | sha256:d9459083f962de6bd980ae6a05be2a4cf670df6a1d898157bceb420342bec280 | 2021-11-12 |
| alpine:3.13 | sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c | 2021-11-12 |
| alpine:3.14 | sha256:635f0aa53d99017b38d1a0aa5b2082f7812b03e3cdb299103fe77b5c8a07f1d2 | 2021-11-12 |
| buildpack-deps:bullseye | sha256:159126fd9e3b05c16a9cc4c2655e4a447effd7892e62b74b933e4a6cb370138e | 2022-01-26 |
| buildpack-deps:buster | sha256:64b00dac65b9ed4db078361a816e24e888e6ae926e43524b79f6fba424e8b6c8 | 2022-01-26 |
| buildpack-deps:stretch | sha256:3004c958ffa89199f04e8735d583c42ee3885c9c77daa4134930fe68eeee59c4 | 2022-01-26 |
| debian:10 | sha256:fde7a280413ec0122bd3a14dc76ba152f89cae999f3b8efe8784100df3640763 | 2022-01-26 |
| debian:11 | sha256:fb45fd4e25abe55a656ca69a7bef70e62099b8bb42a279a5e0ea4ae1ab410e0d | 2022-01-26 |
| debian:9 | sha256:4bb600434787c903886fe33526d19ff33114a33b433a4a4cdbdf9b8543f1ab5d | 2022-01-26 |
| moby/buildkit:latest | sha256:d6c89b7085b106301645ddcc77cf64eb7b705ab507b72d52d130ac33f1300417 | 2021-11-18 |
| node:12 | sha256:7e1786920f4ebd88f8cc2dc9906f3c85ac78674cb29dcced535ff150a96f0873 | 2022-02-04 |
| node:12-alpine | sha256:dfa564312367b1a8fca8db7ae4bae102b28e68b39ebcb7b17022c938f105846b | 2022-02-04 |
| node:14 | sha256:b2c75df8c9706156c38b4f1f678d00e11cb2bfda09fc4ab6e36ec17ac9163865 | 2022-02-02 |
| node:14-alpine | sha256:9a2aa545388a135b496bd55cef2be920b96c4526c99c140170e05a8de3fce653 | 2022-02-02 |
| node:16 | sha256:fd86131ddf8e0faa8ba7a3e49b6bf571745946e663e4065f3bff0a07204c1dde | 2022-02-09 |
| node:16-alpine | sha256:2c6c59cf4d34d4f937ddfcf33bab9d8bbad8658d1b9de7b97622566a52167f2b | 2022-02-09 |
| ubuntu:16.04 | sha256:0f71fa8d4d2d4292c3c617fda2b36f6dabe5c8b6e34c3dc5b0d17d4e704bd39c | 2021-08-31 |
| ubuntu:18.04 | sha256:c2aa13782650aa7ade424b12008128b60034c795f25456e8eb552d0a0f447cad | 2022-02-02 |
| ubuntu:20.04 | sha256:669e010b58baf5beb2836b253c1fd5768333f0d1dbcb834f7c07a4dc93f474be | 2022-02-02 |
### Installed apt packages
| Name | Version |
| ----------------- | --------------------------------- |
| aria2 | 1.33.1-1 |
| binutils | 2.30-21ubuntu1\~18.04.7 |
| bison | 2:3.0.4.dfsg-1build1 |
| brotli | 1.0.3-1ubuntu1.3 |
| build-essential | 12.4ubuntu1 |
| bzip2 | 1.0.6-8.1ubuntu0.2 |
| coreutils | 8.28-1ubuntu1 |
| curl | 7.58.0-2ubuntu3.16 |
| dbus | 1.12.2-1ubuntu1.2 |
| dnsutils | 1:9.11.3+dfsg-1ubuntu1.16 |
| dpkg | 1.19.0.5ubuntu2.3 |
| fakeroot | 1.22-2ubuntu1 |
| file | 1:5.32-2ubuntu0.4 |
| flex | 2.6.4-6 |
| ftp | 0.17-34 |
| gnupg2 | 2.2.4-1ubuntu1.4 |
| haveged | 1.9.1-6 |
| imagemagick | 8:6.9.7.4+dfsg-16ubuntu6.12 |
| iproute2 | 4.15.0-2ubuntu1.3 |
| iputils-ping | 3:20161105-1ubuntu3 |
| jq | 1.5+dfsg-2 |
| lib32z1 | 1:1.2.11.dfsg-0ubuntu2 |
| libc++-dev | 6.0-2 |
| libc++abi-dev | 6.0-2 |
| libcurl3 | 7.58.0-2ubuntu3.16 |
| libgbm-dev | 20.0.8-0ubuntu1\~18.04.1 |
| libgconf-2-4 | 3.2.6-4ubuntu1 |
| libgsl-dev | 2.4+dfsg-6 |
| libgtk-3-0 | 3.22.30-1ubuntu4 |
| libmagic-dev | 1:5.32-2ubuntu0.4 |
| libmagickcore-dev | 8:6.9.7.4+dfsg-16ubuntu6.12 |
| libmagickwand-dev | 8:6.9.7.4+dfsg-16ubuntu6.12 |
| libsecret-1-dev | 0.18.6-1 |
| libsqlite3-dev | 3.22.0-1ubuntu0.4 |
| libunwind8 | 1.2.1-8 |
| libxkbfile-dev | 1:1.0.9-2 |
| libxss1 | 1:1.2.2-1 |
| locales | 2.27-3ubuntu1.4 |
| m4 | 1.4.18-1 |
| mediainfo | 17.12-1 |
| mercurial | 4.5.3-1ubuntu2.2 |
| net-tools | 1.60+git20161116.90da8a0-1ubuntu1 |
| netcat | 1.10-41.1 |
| openssh-client | 1:7.6p1-4ubuntu0.5 |
| p7zip-full | 16.02+dfsg-6 |
| p7zip-rar | 16.02-2 |
| parallel | 20161222-1 |
| pass | 1.7.1-3 |
| patchelf | 0.9-1 |
| pkg-config | 0.29.1-0ubuntu2 |
| pollinate | 4.33-0ubuntu1\~18.04.2 |
| python-setuptools | 39.0.1-2 |
| rpm | 4.14.1+dfsg1-2 |
| rsync | 3.1.2-2.1ubuntu1.2 |
| shellcheck | 0.4.6-1 |
| sphinxsearch | 2.2.11-2 |
| sqlite3 | 3.22.0-1ubuntu0.4 |
| ssh | 1:7.6p1-4ubuntu0.5 |
| sshpass | 1.06-1 |
| subversion | 1.9.7-4ubuntu1 |
| sudo | 1.8.21p2-3ubuntu1.4 |
| swig | 3.0.12-1 |
| telnet | 0.17-41 |
| texinfo | 6.5.0.dfsg.1-2 |
| time | 1.7-25.1build1 |
| tk | 8.6.0+9 |
| tzdata | 2021e-0ubuntu0.18.04 |
| unzip | 6.0-21ubuntu1.1 |
| upx | 3.94-4 |
| wget | 1.19.4-1ubuntu2.2 |
| xorriso | 1.4.8-3 |
| xvfb | 2:1.19.6-1ubuntu4.10 |
| xz-utils | 5.2.2-1.3 |
| zip | 3.0-11build1 |
| zsync | 0.6.2-3ubuntu1 |
+384
View File
@@ -0,0 +1,384 @@
| Announcements |
|-|
| [[All OSs] .NET 2.1 will be removed from the images on February, 21](https://github.com/actions/virtual-environments/issues/4871) |
| [[Ubuntu] Issue with libstdc++ cannot allocate memory in static TLS block](https://github.com/actions/virtual-environments/issues/4799) |
***
# Ubuntu 20.04.3 LTS
- Linux kernel version: 5.11.0-1028-azure
- Image Version: 20220213.1
## Installed Software
### Language and Runtime
- Bash 5.0.17(1)-release
- Clang 10.0.0, 11.0.0, 12.0.0
- Clang-format 10.0.0, 11.0.0, 12.0.0
- Erlang 24.2.1 (Eshell 12.2.1)
- Erlang rebar3 3.18.0
- GNU C++ 9.3.0, 10.3.0
- GNU Fortran 9.3.0, 10.3.0
- Julia 1.7.2
- Kotlin 1.6.10-release-923
- Mono 6.12.0.122 (apt source repository: https://download.mono-project.com/repo/ubuntu stable-focal main)
- MSBuild 16.6.0.15201 (from /usr/lib/mono/msbuild/15.0/bin/MSBuild.dll)
- Node 16.14.0
- Perl 5.30.0
- Python 3.8.10
- Python3 3.8.10
- Ruby 2.7.0p0
- Swift 5.5.3
### Package Management
- cpan 1.64
- Helm 3.8.0
- Homebrew 3.3.14
- Miniconda 4.10.3
- Npm 8.3.1
- Pip 20.0.2
- Pip3 20.0.2
- Pipx 1.0.0
- RubyGems 3.1.2
- Vcpkg (build from master \<8dbd66f5a>)
- Yarn 1.22.17
#### Environment variables
| Name | Value |
| ----------------------- | ---------------------- |
| CONDA | /usr/share/miniconda |
| VCPKG_INSTALLATION_ROOT | /usr/local/share/vcpkg |
### Project Management
- Ant 1.10.7
- Gradle 7.4
- Lerna 4.0.0
- Maven 3.8.4
- Sbt 1.6.2
### Tools
- Ansible 2.12.2
- apt-fast 1.9.12
- AzCopy 10.13.0 (available by `azcopy` and `azcopy10` aliases)
- Bazel 5.0.0
- Bazelisk 1.11.0
- Bicep 0.4.1272
- Buildah 1.21.3 (apt source repository: https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable)
- CMake 3.22.2
- CodeQL Action Bundle 2.8.0
- Docker Compose v1 1.29.2
- Docker Compose v2 2.2.3+azure-1
- Docker-Buildx 0.7.1
- Docker-Moby Client 20.10.11+azure-3
- Docker-Moby Server 20.10.11+azure-3
- Fastlane 2.204.3
- Git 2.35.1 (apt source repository: ppa:git-core/ppa)
- Git LFS 3.0.2 (apt source repository: https://packagecloud.io/install/repositories/github/git-lfs)
- Git-ftp 1.6.0
- Haveged 1.9.1
- Heroku 7.59.2
- HHVM (HipHop VM) 4.148.0
- jq 1.6
- Kind 0.11.1
- Kubectl 1.23.3
- Kustomize 4.5.2
- Leiningen 2.9.8
- MediaInfo 19.09
- Mercurial 5.3.1
- Minikube 1.25.1
- n 8.0.2
- Newman 5.3.1
- nvm 0.39.1
- OpenSSL 1.1.1f 31 Mar 2020
- Packer 1.7.10
- Parcel 2.3.1
- PhantomJS 2.1.1
- Podman 3.4.2 (apt source repository: https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable)
- Pulumi 3.24.1
- R 4.1.2
- Skopeo 1.3.0 (apt source repository: https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable)
- Sphinx Open Source Search Server 2.2.11
- SVN 1.13.0
- Terraform 1.1.5
- yamllint 1.26.3
- yq 4.19.1
- zstd 1.5.2 (homebrew)
### CLI Tools
- Alibaba Cloud CLI 3.0.107
- AWS CLI 2.4.18
- AWS CLI Session manager plugin 1.2.295.0
- AWS SAM CLI 1.37.0
- Azure CLI (azure-cli) 2.33.0 (installation method: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt)
- Azure CLI (azure-devops) 0.23.0
- GitHub CLI 2.5.0
- Google Cloud SDK 369.0.0 (apt source repository: https://packages.cloud.google.com/apt)
- Hub CLI 2.14.2
- Netlify CLI 9.4.0
- OpenShift CLI 4.9.19
- ORAS CLI 0.12.0
- Vercel CLI 23.1.2
### Java
| Version | Vendor | Environment Variable |
| --------------------- | --------------- | -------------------- |
| 8.0.322+6 | Eclipse Temurin | JAVA_HOME_8_X64 |
| 11.0.14.1+1 (default) | Eclipse Temurin | JAVA_HOME_11_X64 |
| 17.0.2+8 | Eclipse Temurin | JAVA_HOME_17_X64 |
### GraalVM
| Version | Environment variables |
| ----------- | --------------------- |
| CE 22.0.0.2 | GRAALVM_11_ROOT |
### PHP
| Tool | Version |
| -------- | ------------------- |
| PHP | 7.4.27 8.0.15 8.1.2 |
| Composer | 2.2.6 |
| PHPUnit | 8.5.23 |
```
Both Xdebug and PCOV extensions are installed, but only Xdebug is enabled.
```
### Haskell
- Cabal 3.6.2.0
- GHC 9.2.1
- GHCup 0.1.17.4
- Stack 2.7.3
### Rust Tools
- Cargo 1.58.0
- Rust 1.58.1
- Rustdoc 1.58.1
- Rustup 1.24.3
#### Packages
- Bindgen 0.59.2
- Cargo audit 0.16.0
- Cargo clippy 0.1.58
- Cargo outdated 0.10.2
- Cbindgen 0.20.0
- Rustfmt 1.4.38
### Browsers and Drivers
- Google Chrome 98.0.4758.80
- ChromeDriver 98.0.4758.80
- Mozilla Firefox 96.0
- Geckodriver 0.30.0
- Chromium 98.0.4758.0
- Selenium server 4.1.0
#### Environment variables
| Name | Value |
| ----------------- | ----------------------------------- |
| CHROMEWEBDRIVER | /usr/local/share/chrome_driver |
| GECKOWEBDRIVER | /usr/local/share/gecko_driver |
| SELENIUM_JAR_PATH | /usr/share/java/selenium-server.jar |
### .NET Core SDK
- 2.1.302 2.1.403 2.1.526 2.1.617 2.1.701 2.1.818 3.1.120 3.1.202 3.1.302 3.1.416 5.0.104 5.0.211 5.0.303 5.0.405 6.0.102
### .NET tools
- nbgv 3.4.255+06fb9182bf
### Databases
- MongoDB 5.0.6 (apt source repository: https://repo.mongodb.org/apt/ubuntu)
- PostgreSQL 14.2 (apt source repository: https://apt.postgresql.org/pub/repos/apt/)
- sqlite3 3.31.1
#### MySQL
- MySQL 8.0.26
- MySQL Server (user:root password:root)
```
MySQL service is disabled by default. Use the following command as a part of your job to start the service: 'sudo systemctl start mysql.service'
```
#### MS SQL Server Client Tools
- sqlcmd 17.8.0001.1
- SqlPackage 16.0.5400.1
### Cached Tools
#### Go
- 1.15.15
- 1.16.14
- 1.17.7
#### Node.js
- 12.22.10
- 14.19.0
- 16.14.0
#### PyPy
- 2.7.18 [PyPy 7.3.6]
- 3.6.12 [PyPy 7.3.3]
- 3.7.12 [PyPy 7.3.7]
- 3.8.12 [PyPy 7.3.7]
#### Python
- 2.7.18
- 3.6.15
- 3.7.12
- 3.8.12
- 3.9.10
- 3.10.2
#### Ruby
- 2.5.9
- 2.6.9
- 2.7.5
- 3.0.3
#### Environment variables
| Name | Value | Architecture |
| --------------- | ----------------------------------- | ------------ |
| GOROOT_1_15_X64 | /opt/hostedtoolcache/go/1.15.15/x64 | x64 |
| GOROOT_1_16_X64 | /opt/hostedtoolcache/go/1.16.14/x64 | x64 |
| GOROOT_1_17_X64 | /opt/hostedtoolcache/go/1.17.7/x64 | x64 |
### PowerShell Tools
- PowerShell 7.2.1
#### PowerShell Modules
| Module | Version |
| ---------------- | ------- |
| MarkdownPS | 1.9 |
| Pester | 5.3.1 |
| PSScriptAnalyzer | 1.20.0 |
#### Az PowerShell Modules
- 7.1.0 3.1.0.zip 4.4.0.zip 5.9.0.zip 6.6.0.zip
### Web Servers
| Name | Version | ConfigFile | ServiceStatus | ListenPort |
| --------- | ------- | ------------------------- | ------------- | ---------- |
| apache2 | 2.4.41 | /etc/apache2/apache2.conf | inactive | 80 |
| mono-xsp4 | 4.7.1 | /etc/default/mono-xsp4 | active | 8084 |
| nginx | 1.18.0 | /etc/nginx/nginx.conf | inactive | 80 |
### Android
| Package Name | Version |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Android Command Line Tools | 4.0 |
| Android Emulator | 31.2.8 |
| Android SDK Build-tools | 32.0.0<br>31.0.0<br>30.0.0 30.0.1 30.0.2 30.0.3<br>29.0.0 29.0.1 29.0.2 29.0.3<br>28.0.0 28.0.1 28.0.2 28.0.3<br>27.0.0 27.0.1 27.0.2 27.0.3 |
| Android SDK Platform-Tools | 32.0.0 |
| Android SDK Platforms | android-32 (rev 1)<br>android-31 (rev 1)<br>android-30 (rev 3)<br>android-29 (rev 5)<br>android-28 (rev 6)<br>android-27 (rev 3) |
| Android SDK Tools | 26.1.1 |
| Android Support Repository | 47.0.0 |
| CMake | 3.10.2<br>3.18.1 |
| Google Play services | 49 |
| Google Repository | 58 |
| NDK | 21.4.7075529 (default)<br>22.1.7171670<br>23.1.7779620 |
| SDK Patch Applier v4 | 1 |
#### Environment variables
| Name | Value |
| ----------------------- | ------------------------------------------------------------------------------------ |
| ANDROID_HOME | /usr/local/lib/android/sdk |
| ANDROID_NDK_HOME | /usr/local/lib/android/sdk/ndk-bundle -> /usr/local/lib/android/sdk/ndk/21.4.7075529 |
| ANDROID_NDK_LATEST_HOME | /usr/local/lib/android/sdk/ndk/23.1.7779620 |
| ANDROID_NDK_ROOT | /usr/local/lib/android/sdk/ndk-bundle -> /usr/local/lib/android/sdk/ndk/21.4.7075529 |
| ANDROID_SDK_ROOT | /usr/local/lib/android/sdk |
### Cached Docker images
| Repository:Tag | Digest | Created |
| ----------------------- | ------------------------------------------------------------------------ | ---------- |
| alpine:3.12 | sha256:d9459083f962de6bd980ae6a05be2a4cf670df6a1d898157bceb420342bec280 | 2021-11-12 |
| alpine:3.13 | sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c | 2021-11-12 |
| alpine:3.14 | sha256:635f0aa53d99017b38d1a0aa5b2082f7812b03e3cdb299103fe77b5c8a07f1d2 | 2021-11-12 |
| buildpack-deps:bullseye | sha256:159126fd9e3b05c16a9cc4c2655e4a447effd7892e62b74b933e4a6cb370138e | 2022-01-26 |
| buildpack-deps:buster | sha256:64b00dac65b9ed4db078361a816e24e888e6ae926e43524b79f6fba424e8b6c8 | 2022-01-26 |
| buildpack-deps:stretch | sha256:3004c958ffa89199f04e8735d583c42ee3885c9c77daa4134930fe68eeee59c4 | 2022-01-26 |
| debian:10 | sha256:fde7a280413ec0122bd3a14dc76ba152f89cae999f3b8efe8784100df3640763 | 2022-01-26 |
| debian:11 | sha256:fb45fd4e25abe55a656ca69a7bef70e62099b8bb42a279a5e0ea4ae1ab410e0d | 2022-01-26 |
| debian:9 | sha256:4bb600434787c903886fe33526d19ff33114a33b433a4a4cdbdf9b8543f1ab5d | 2022-01-26 |
| moby/buildkit:latest | sha256:d6c89b7085b106301645ddcc77cf64eb7b705ab507b72d52d130ac33f1300417 | 2021-11-18 |
| node:12 | sha256:7e1786920f4ebd88f8cc2dc9906f3c85ac78674cb29dcced535ff150a96f0873 | 2022-02-04 |
| node:12-alpine | sha256:dfa564312367b1a8fca8db7ae4bae102b28e68b39ebcb7b17022c938f105846b | 2022-02-04 |
| node:14 | sha256:b2c75df8c9706156c38b4f1f678d00e11cb2bfda09fc4ab6e36ec17ac9163865 | 2022-02-02 |
| node:14-alpine | sha256:9a2aa545388a135b496bd55cef2be920b96c4526c99c140170e05a8de3fce653 | 2022-02-02 |
| node:16 | sha256:fd86131ddf8e0faa8ba7a3e49b6bf571745946e663e4065f3bff0a07204c1dde | 2022-02-09 |
| node:16-alpine | sha256:2c6c59cf4d34d4f937ddfcf33bab9d8bbad8658d1b9de7b97622566a52167f2b | 2022-02-09 |
| ubuntu:16.04 | sha256:0f71fa8d4d2d4292c3c617fda2b36f6dabe5c8b6e34c3dc5b0d17d4e704bd39c | 2021-08-31 |
| ubuntu:18.04 | sha256:c2aa13782650aa7ade424b12008128b60034c795f25456e8eb552d0a0f447cad | 2022-02-02 |
| ubuntu:20.04 | sha256:669e010b58baf5beb2836b253c1fd5768333f0d1dbcb834f7c07a4dc93f474be | 2022-02-02 |
### Installed apt packages
| Name | Version |
| ---------------------- | --------------------------------- |
| acl | 2.2.53-6 |
| aria2 | 1.35.0-1build1 |
| binutils | 2.34-6ubuntu1.3 |
| bison | 2:3.5.1+dfsg-1 |
| brotli | 1.0.7-6ubuntu0.1 |
| build-essential | 12.8ubuntu1.1 |
| bzip2 | 1.0.8-2 |
| coreutils | 8.30-3ubuntu2 |
| curl | 7.68.0-1ubuntu2.7 |
| dbus | 1.12.16-2ubuntu2.1 |
| dnsutils | 1:9.16.1-0ubuntu2.9 |
| dpkg | 1.19.7ubuntu3 |
| fakeroot | 1.24-1 |
| file | 1:5.38-4 |
| flex | 2.6.4-6.2 |
| fonts-noto-color-emoji | 0\~20200916-1\~ubuntu20.04.1 |
| ftp | 0.17-34.1 |
| gnupg2 | 2.2.19-3ubuntu2.1 |
| haveged | 1.9.1-6ubuntu1 |
| imagemagick | 8:6.9.10.23+dfsg-2.1ubuntu11.4 |
| iproute2 | 5.5.0-1ubuntu1 |
| iputils-ping | 3:20190709-3 |
| jq | 1.6-1ubuntu0.20.04.1 |
| lib32z1 | 1:1.2.11.dfsg-2ubuntu1.2 |
| libc++-dev | 1:10.0-50\~exp1 |
| libc++abi-dev | 1:10.0-50\~exp1 |
| libcurl4 | 7.68.0-1ubuntu2.7 |
| libgbm-dev | 21.2.6-0ubuntu0.1\~20.04.1 |
| libgconf-2-4 | 3.2.6-6ubuntu1 |
| libgsl-dev | 2.5+dfsg-6build1 |
| libgtk-3-0 | 3.24.20-0ubuntu1 |
| libmagic-dev | 1:5.38-4 |
| libmagickcore-dev | 8:6.9.10.23+dfsg-2.1ubuntu11.4 |
| libmagickwand-dev | 8:6.9.10.23+dfsg-2.1ubuntu11.4 |
| libsecret-1-dev | 0.20.4-0ubuntu1 |
| libsqlite3-dev | 3.31.1-4ubuntu0.2 |
| libunwind8 | 1.2.1-9build1 |
| libxkbfile-dev | 1:1.1.0-1 |
| libxss1 | 1:1.2.3-1 |
| locales | 2.31-0ubuntu9.2 |
| m4 | 1.4.18-4 |
| mediainfo | 19.09-1build1 |
| mercurial | 5.3.1-1ubuntu1 |
| net-tools | 1.60+git20180626.aebd88e-1ubuntu1 |
| netcat | 1.206-1ubuntu1 |
| openssh-client | 1:8.2p1-4ubuntu0.4 |
| p7zip-full | 16.02+dfsg-7build1 |
| p7zip-rar | 16.02-3build1 |
| parallel | 20161222-1.1 |
| pass | 1.7.3-2 |
| patchelf | 0.10-2build1 |
| pkg-config | 0.29.1-0ubuntu4 |
| pollinate | 4.33-3ubuntu1.20.04.1 |
| python-is-python3 | 3.8.2-4 |
| rpm | 4.14.2.1+dfsg1-1build2 |
| rsync | 3.1.3-8ubuntu0.1 |
| shellcheck | 0.7.0-2build2 |
| sphinxsearch | 2.2.11-2ubuntu2 |
| sqlite3 | 3.31.1-4ubuntu0.2 |
| ssh | 1:8.2p1-4ubuntu0.4 |
| sshpass | 1.06-1 |
| subversion | 1.13.0-3 |
| sudo | 1.8.31-1ubuntu1.2 |
| swig | 4.0.1-5build1 |
| telnet | 0.17-41.2build1 |
| texinfo | 6.7.0.dfsg.2-5 |
| time | 1.7-25.1build1 |
| tk | 8.6.9+1 |
| tzdata | 2021e-0ubuntu0.20.04 |
| unzip | 6.0-25ubuntu1 |
| upx | 3.95-2build1 |
| wget | 1.20.3-1ubuntu2 |
| xorriso | 1.5.2-1 |
| xvfb | 2:1.20.13-1ubuntu1\~20.04.2 |
| xz-utils | 5.2.4-1ubuntu1 |
| zip | 3.0-11build1 |
| zsync | 0.6.2-3ubuntu1 |
@@ -1,2 +1,2 @@
# Name of pool supported by this image
POOL_NAME="Ubuntu 2204"
POOL_NAME="Ubuntu 1804"
+2
View File
@@ -0,0 +1,2 @@
# Name of pool supported by this image
POOL_NAME="Ubuntu 2004"
@@ -20,6 +20,7 @@ function Get-AndroidInstalledPackages {
return $androidSDKManagerList
}
function Build-AndroidTable {
$packageInfo = Get-AndroidInstalledPackages
return @(
@@ -43,6 +44,10 @@ function Build-AndroidTable {
"Package" = "Android SDK Platforms"
"Version" = Get-AndroidPlatformVersions -PackageInfo $packageInfo
},
@{
"Package" = "Android SDK Tools"
"Version" = Get-AndroidPackageVersions -PackageInfo $packageInfo -MatchedString "Android SDK Tools"
},
@{
"Package" = "Android Support Repository"
"Version" = Get-AndroidPackageVersions -PackageInfo $packageInfo -MatchedString "Android Support Repository"
@@ -74,7 +79,7 @@ function Build-AndroidTable {
) | Where-Object { $_.Version } | ForEach-Object {
[PSCustomObject] @{
"Package Name" = $_.Package
"Version" = $_.Version
"Version" = $_.Version
}
}
}
@@ -87,7 +92,7 @@ function Get-AndroidPackageVersions {
[object] $MatchedString
)
$versions = $PackageInfo | Where-Object { $_ -Match $MatchedString } | ForEach-Object {
$versions = $packageInfo | Where-Object { $_ -Match $MatchedString } | ForEach-Object {
$packageInfoParts = Split-TableRowByColumns $_
return $packageInfoParts[1]
}
@@ -100,7 +105,7 @@ function Get-AndroidPlatformVersions {
[object] $PackageInfo
)
$versions = $PackageInfo | Where-Object { $_ -Match "Android SDK Platform " } | ForEach-Object {
$versions = $packageInfo | Where-Object { $_ -Match "Android SDK Platform " } | ForEach-Object {
$packageInfoParts = Split-TableRowByColumns $_
$revision = $packageInfoParts[1]
$version = $packageInfoParts[0].split(";")[1]
@@ -123,7 +128,7 @@ function Get-AndroidBuildToolVersions {
[object] $PackageInfo
)
$versions = $PackageInfo | Where-Object { $_ -Match "Android SDK Build-Tools" } | ForEach-Object {
$versions = $packageInfo | Where-Object { $_ -Match "Android SDK Build-Tools" } | ForEach-Object {
$packageInfoParts = Split-TableRowByColumns $_
return $packageInfoParts[1]
}
@@ -141,7 +146,7 @@ function Get-AndroidGoogleAPIsVersions {
[object] $PackageInfo
)
$versions = $PackageInfo | Where-Object { $_ -Match "Google APIs" } | ForEach-Object {
$versions = $packageInfo | Where-Object { $_ -Match "Google APIs" } | ForEach-Object {
$packageInfoParts = Split-TableRowByColumns $_
return $packageInfoParts[0].split(";")[1]
}
@@ -151,7 +156,7 @@ function Get-AndroidGoogleAPIsVersions {
function Get-AndroidNDKVersions {
$ndkFolderPath = Join-Path (Get-AndroidSDKRoot) "ndk"
$versions = Get-ChildItem -Path $ndkFolderPath -Name
$ndkDefaultVersion = (Get-ToolsetContent).android.ndk.default
$ndkDefaultVersion = Get-ToolsetValue "android.ndk.default"
$ndkDefaultFullVersion = Get-ChildItem "$env:ANDROID_HOME/ndk/$ndkDefaultVersion.*" -Name | Select-Object -Last 1
return ($versions | ForEach-Object {
@@ -161,12 +166,12 @@ function Get-AndroidNDKVersions {
}
function Build-AndroidEnvironmentTable {
$androidVersions = Get-Item env:ANDROID_*
$androidVersions = Get-Item env:ANDROID_*
$shouldResolveLink = 'ANDROID_NDK', 'ANDROID_NDK_HOME', 'ANDROID_NDK_ROOT', 'ANDROID_NDK_LATEST_HOME'
$shouldResolveLink = 'ANDROID_NDK_PATH', 'ANDROID_NDK_HOME', 'ANDROID_NDK_ROOT', 'ANDROID_NDK_LATEST_HOME'
return $androidVersions | Sort-Object -Property Name | ForEach-Object {
[PSCustomObject] @{
"Name" = $_.Name
"Name" = $_.Name
"Value" = if ($shouldResolveLink.Contains($_.Name )) { Get-PathWithLink($_.Value) } else {$_.Value}
}
}
@@ -0,0 +1,52 @@
function Get-ChromeVersion {
$googleChromeVersion = google-chrome --version | Take-OutputPart -Part 2
return "Google Chrome $googleChromeVersion"
}
function Get-ChromeDriverVersion {
$chromeDriverVersion = chromedriver --version | Take-OutputPart -Part 1
return "ChromeDriver $chromeDriverVersion"
}
function Get-FirefoxVersion {
$firefoxVersion = firefox --version
return $firefoxVersion
}
function Get-GeckodriverVersion {
$geckodriverVersion = geckodriver --version | Select-Object -First 1 | Take-OutputPart -Part 1
return "Geckodriver $geckodriverVersion"
}
function Get-ChromiumVersion {
$chromiumVersion = chromium-browser --version | Take-OutputPart -Part 0,1
return $chromiumVersion
}
function Get-SeleniumVersion {
$seleniumBinaryName = Get-ToolsetValue "selenium.binary_name"
$fullSeleniumVersion = (Get-ChildItem "/usr/share/java/${seleniumBinaryName}-*").Name -replace "${seleniumBinaryName}-"
return "Selenium server $fullSeleniumVersion"
}
function Build-BrowserWebdriversEnvironmentTable {
return @(
@{
"Name" = "CHROMEWEBDRIVER"
"Value" = $env:CHROMEWEBDRIVER
},
@{
"Name" = "GECKOWEBDRIVER"
"Value" = $env:GECKOWEBDRIVER
},
@{
"Name" = "SELENIUM_JAR_PATH"
"Value" = $env:SELENIUM_JAR_PATH
}
) | ForEach-Object {
[PSCustomObject] @{
"Name" = $_.Name
"Value" = $_.Value
}
}
}
@@ -0,0 +1,63 @@
function Get-ToolcacheRubyVersions {
$toolcachePath = Join-Path $env:AGENT_TOOLSDIRECTORY "Ruby"
return Get-ChildItem $toolcachePath -Name | Sort-Object { [Version]$_ }
}
function Get-ToolcachePythonVersions {
$toolcachePath = Join-Path $env:AGENT_TOOLSDIRECTORY "Python"
return Get-ChildItem $toolcachePath -Name | Sort-Object { [Version]$_ }
}
function Get-ToolcachePyPyVersions {
$toolcachePath = Join-Path $env:AGENT_TOOLSDIRECTORY "PyPy"
Get-ChildItem -Path $toolcachePath -Name | Sort-Object { [Version] $_ } | ForEach-Object {
$pypyRootPath = Join-Path $toolcachePath $_ "x64"
[string]$pypyVersionOutput = & "$pypyRootPath/bin/python" -c "import sys;print(sys.version)"
$pypyVersionOutput -match "^([\d\.]+) \(.+\) \[PyPy ([\d\.]+\S*) .+]$" | Out-Null
return "{0} [PyPy {1}]" -f $Matches[1], $Matches[2]
}
}
function Get-ToolcacheNodeVersions {
$toolcachePath = Join-Path $env:AGENT_TOOLSDIRECTORY "node"
return Get-ChildItem $toolcachePath -Name | Sort-Object { [Version]$_ }
}
function Get-ToolcacheGoVersions {
$toolcachePath = Join-Path $env:AGENT_TOOLSDIRECTORY "go"
return Get-ChildItem $toolcachePath -Name | Sort-Object { [Version]$_ }
}
function Build-GoEnvironmentTable {
return Get-CachedToolInstances -Name "go" -VersionCommand "version" | ForEach-Object {
$Version = [System.Version]($_.Version -Split(" "))[0]
$Name = "GOROOT_$($Version.major)_$($Version.minor)_X64"
$Value = (Get-Item env:\$Name).Value
[PSCustomObject] @{
"Name" = $Name
"Value" = (Get-Item env:\$Name).Value
"Architecture" = $_. Architecture
}
}
}
function Build-CachedToolsSection {
$output = ""
$output += New-MDHeader "Go" -Level 4
$output += New-MDList -Lines (Get-ToolcacheGoVersions) -Style Unordered
$output += New-MDHeader "Node.js" -Level 4
$output += New-MDList -Lines (Get-ToolcacheNodeVersions) -Style Unordered
$output += New-MDHeader "PyPy" -Level 4
$output += New-MDList -Lines (Get-ToolcachePyPyVersions) -Style Unordered
$output += New-MDHeader "Python" -Level 4
$output += New-MDList -Lines (Get-ToolcachePythonVersions) -Style Unordered
$output += New-MDHeader "Ruby" -Level 4
$output += New-MDList -Lines (Get-ToolcacheRubyVersions) -Style Unordered
return $output
}
@@ -0,0 +1,423 @@
function Get-BashVersion {
$version = bash -c 'echo ${BASH_VERSION}'
return "Bash $version"
}
function Get-CPPVersions {
$result = Get-CommandResult "apt list --installed" -Multiline
$cppVersions = $result.Output | Where-Object { $_ -match "g\+\+-\d+"} | ForEach-Object {
& $_.Split("/")[0] --version | Select-Object -First 1 | Take-OutputPart -Part 3
} | Sort-Object {[Version]$_}
return "GNU C++ " + ($cppVersions -Join ", ")
}
function Get-FortranVersions {
$result = Get-CommandResult "apt list --installed" -Multiline
$fortranVersions = $result.Output | Where-Object { $_ -match "^gfortran-\d+"} | ForEach-Object {
$_ -match "now (?<version>\d+\.\d+\.\d+)-" | Out-Null
$Matches.version
} | Sort-Object {[Version]$_}
return "GNU Fortran " + ($fortranVersions -Join ", ")
}
function Get-ClangToolVersions {
param (
[Parameter(Mandatory = $true)]
[string] $ToolName,
[string] $VersionPattern = "\d+\.\d+\.\d+)-"
)
$result = Get-CommandResult "apt list --installed" -Multiline
$toolVersions = $result.Output | Where-Object { $_ -match "^${ToolName}-\d+"} | ForEach-Object {
$clangCommand = ($_ -Split "/")[0]
Invoke-Expression "$clangCommand --version" | Where-Object { $_ -match "${ToolName} version" } | ForEach-Object {
$_ -match "${ToolName} version (?<version>${VersionPattern}" | Out-Null
$Matches.version
}
} | Sort-Object {[Version]$_}
return $toolVersions -Join ", "
}
function Get-ClangVersions {
$clangVersions = Get-ClangToolVersions -ToolName "clang"
return "Clang " + $clangVersions
}
function Get-ClangFormatVersions {
$clangFormatVersions = Get-ClangToolVersions -ToolName "clang-format"
return "Clang-format " + $clangFormatVersions
}
function Get-ErlangVersion {
$erlangVersion = (erl -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), ''OTP_VERSION''])), io:fwrite(Version), halt().' -noshell)
$shellVersion = (erl -eval 'erlang:display(erlang:system_info(version)), halt().' -noshell).Trim('"')
return "Erlang $erlangVersion (Eshell $shellVersion)"
}
function Get-ErlangRebar3Version {
$result = Get-CommandResult "rebar3 --version"
$result.Output -match "rebar (?<version>(\d+.){2}\d+)" | Out-Null
$rebarVersion = $Matches.version
return "Erlang rebar3 $rebarVersion"
}
function Get-MonoVersion {
$monoVersion = mono --version | Out-String | Take-OutputPart -Part 4
$aptSourceRepo = Get-AptSourceRepository -PackageName "mono"
return "Mono $monoVersion (apt source repository: $aptSourceRepo)"
}
function Get-MsbuildVersion {
$msbuildVersion = msbuild -version | Select-Object -Last 1
$result = Select-String -Path (Get-Command msbuild).Source -Pattern "msbuild"
$result -match "(?<path>\/\S*\.dll)" | Out-Null
$msbuildPath = $Matches.path
return "MSBuild $msbuildVersion (from $msbuildPath)"
}
function Get-NodeVersion {
$nodeVersion = $(node --version).Substring(1)
return "Node $nodeVersion"
}
function Get-OpensslVersion {
return $(openssl version)
}
function Get-PerlVersion {
$version = $(perl -e 'print substr($^V,1)')
return "Perl $version"
}
function Get-PythonVersion {
$result = Get-CommandResult "python --version"
$version = $result.Output | Take-OutputPart -Part 1
return "Python $version"
}
function Get-Python3Version {
$result = Get-CommandResult "python3 --version"
$version = $result.Output | Take-OutputPart -Part 1
return "Python3 $version"
}
function Get-PowershellVersion {
return $(pwsh --version)
}
function Get-RubyVersion {
$rubyVersion = ruby --version | Out-String | Take-OutputPart -Part 1
return "Ruby $rubyVersion"
}
function Get-SwiftVersion {
$swiftVersion = swift --version | Out-String | Take-OutputPart -Part 2
return "Swift $swiftVersion"
}
function Get-KotlinVersion {
$kotlinVersion = kotlin -version | Out-String | Take-OutputPart -Part 2
return "Kotlin $kotlinVersion"
}
function Get-JuliaVersion {
$juliaVersion = julia --version | Take-OutputPart -Part 2
return "Julia $juliaVersion"
}
function Get-LernaVersion {
$version = lerna -v
return "Lerna $version"
}
function Get-HomebrewVersion {
$result = Get-CommandResult "brew -v"
$result.Output -match "Homebrew (?<version>\d+\.\d+\.\d+)" | Out-Null
$version = $Matches.version
return "Homebrew $version"
}
function Get-CpanVersion {
$result = Get-CommandResult "cpan --version" -ExpectExitCode @(25, 255)
$result.Output -match "version (?<version>\d+\.\d+) " | Out-Null
$cpanVersion = $Matches.version
return "cpan $cpanVersion"
}
function Get-GemVersion {
$result = Get-CommandResult "gem --version"
$result.Output -match "(?<version>\d+\.\d+\.\d+)" | Out-Null
$gemVersion = $Matches.version
return "RubyGems $gemVersion"
}
function Get-MinicondaVersion {
$condaVersion = conda --version
return "Mini$condaVersion"
}
function Get-HelmVersion {
$(helm version) -match 'Version:"v(?<version>\d+\.\d+\.\d+)"' | Out-Null
$helmVersion = $Matches.version
return "Helm $helmVersion"
}
function Get-NpmVersion {
$npmVersion = npm --version
return "Npm $npmVersion"
}
function Get-YarnVersion {
$yarnVersion = yarn --version
return "Yarn $yarnVersion"
}
function Get-ParcelVersion {
$parcelVersion = parcel --version
return "Parcel $parcelVersion"
}
function Get-PipVersion {
$result = Get-CommandResult "pip --version"
$result.Output -match "pip (?<version>\d+\.\d+\.\d+)" | Out-Null
$pipVersion = $Matches.version
return "Pip $pipVersion"
}
function Get-Pip3Version {
$result = Get-CommandResult "pip3 --version"
$result.Output -match "pip (?<version>\d+\.\d+\.\d+)" | Out-Null
$pipVersion = $Matches.version
return "Pip3 $pipVersion"
}
function Get-VcpkgVersion {
$result = Get-CommandResult "vcpkg version"
$result.Output -match "version (?<version>\d+\.\d+\.\d+)" | Out-Null
$vcpkgVersion = $Matches.version
$commitId = git -C "/usr/local/share/vcpkg" rev-parse --short HEAD
return "Vcpkg $vcpkgVersion (build from master \<$commitId>)"
}
function Get-AntVersion {
$result = ant -version | Out-String
$result -match "version (?<version>\d+\.\d+\.\d+)" | Out-Null
$antVersion = $Matches.version
return "Ant $antVersion"
}
function Get-GradleVersion {
$gradleVersion = (gradle -v) -match "^Gradle \d" | Take-OutputPart -Part 1
return "Gradle $gradleVersion"
}
function Get-MavenVersion {
$result = mvn -version | Out-String
$result -match "Apache Maven (?<version>\d+\.\d+\.\d+)" | Out-Null
$mavenVersion = $Matches.version
return "Maven $mavenVersion"
}
function Get-SbtVersion {
$result = Get-CommandResult "sbt -version"
$result.Output -match "sbt script version: (?<version>\d+\.\d+\.\d+)" | Out-Null
$sbtVersion = $Matches.version
return "Sbt $sbtVersion"
}
function Get-PHPVersions {
$result = Get-CommandResult "apt list --installed" -Multiline
return $result.Output | Where-Object { $_ -match "^php\d+\.\d+/"} | ForEach-Object {
$_ -match "now (?<version>\d+\.\d+\.\d+)-" | Out-Null
$Matches.version
}
}
function Get-ComposerVersion {
$(composer --version) -match "Composer version (?<version>\d+\.\d+\.\d+)\s" | Out-Null
return $Matches.version
}
function Get-PHPUnitVersion {
$(phpunit --version | Out-String) -match "PHPUnit (?<version>\d+\.\d+\.\d+)\s" | Out-Null
return $Matches.version
}
function Build-PHPTable {
$php = @{
"Tool" = "PHP"
"Version" = "$(Get-PHPVersions -Join '<br>')"
}
$composer = @{
"Tool" = "Composer"
"Version" = Get-ComposerVersion
}
$phpunit = @{
"Tool" = "PHPUnit"
"Version" = Get-PHPUnitVersion
}
return @($php, $composer, $phpunit) | ForEach-Object {
[PSCustomObject] @{
"Tool" = $_.Tool
"Version" = $_.Version
}
}
}
function Build-PHPSection {
$output = ""
$output += New-MDHeader "PHP" -Level 3
$output += Build-PHPTable | New-MDTable
$output += New-MDCode -Lines @(
"Both Xdebug and PCOV extensions are installed, but only Xdebug is enabled."
)
return $output
}
function Get-GHCVersion {
$(ghc --version) -match "version (?<version>\d+\.\d+\.\d+)" | Out-Null
$ghcVersion = $Matches.version
return "GHC $ghcVersion"
}
function Get-GHCupVersion {
$(ghcup --version) -match "version v(?<version>\d+(\.\d+){2,})" | Out-Null
$ghcVersion = $Matches.version
return "GHCup $ghcVersion"
}
function Get-CabalVersion {
$(cabal --version | Out-String) -match "cabal-install version (?<version>\d+\.\d+\.\d+\.\d+)" | Out-Null
$cabalVersion = $Matches.version
return "Cabal $cabalVersion"
}
function Get-StackVersion {
$(stack --version | Out-String) -match "Version (?<version>\d+\.\d+\.\d+)" | Out-Null
$stackVersion = $Matches.version
return "Stack $stackVersion"
}
function Get-AzModuleVersions {
$azModuleVersions = Get-ChildItem /usr/share | Where-Object { $_ -match "az_\d+" } | Foreach-Object {
$_.Name.Split("_")[1]
}
$azModuleVersions = $azModuleVersions -join " "
return $azModuleVersions
}
function Get-PowerShellModules {
$modules = (Get-ToolsetContent).powershellModules.name
$psModules = Get-Module -Name $modules -ListAvailable | Sort-Object Name | Group-Object Name
$psModules | ForEach-Object {
$moduleName = $_.Name
$moduleVersions = ($_.group.Version | Sort-Object -Unique) -join '<br>'
[PSCustomObject]@{
Module = $moduleName
Version = $moduleVersions
}
}
}
function Get-DotNetCoreSdkVersions {
$unsortedDotNetCoreSdkVersion = dotnet --list-sdks list | ForEach-Object { $_ | Take-OutputPart -Part 0 }
$dotNetCoreSdkVersion = $unsortedDotNetCoreSdkVersion -join " "
return $dotNetCoreSdkVersion
}
function Get-DotnetTools {
$env:PATH = "/etc/skel/.dotnet/tools:$($env:PATH)"
$dotnetTools = (Get-ToolsetContent).dotnet.tools
$toolsList = @()
ForEach ($dotnetTool in $dotnetTools) {
$toolsList += $dotnetTool.name + " " + (Invoke-Expression $dotnetTool.getversion)
}
return $toolsList
}
function Get-CachedDockerImages {
$toolsetJson = Get-ToolsetContent
$images = $toolsetJson.docker.images
return $images
}
function Get-CachedDockerImagesTableData {
$allImages = sudo docker images --digests --format "*{{.Repository}}:{{.Tag}}|{{.Digest}} |{{.CreatedAt}}"
$allImages.Split("*") | Where-Object { $_ } | ForEach-Object {
$parts = $_.Split("|")
[PSCustomObject] @{
"Repository:Tag" = $parts[0]
"Digest" = $parts[1]
"Created" = $parts[2].split(' ')[0]
}
} | Sort-Object -Property "Repository:Tag"
}
function Get-AptPackages {
$apt = (Get-ToolsetContent).Apt
$output = @()
ForEach ($pkg in ($apt.common_packages + $apt.cmd_packages)) {
$version = $(dpkg-query -W -f '${Version}' $pkg)
if ($Null -eq $version) {
$version = $(dpkg-query -W -f '${Version}' "$pkg*")
}
$version = $version -replace '~','\~'
$output += [PSCustomObject] @{
Name = $pkg
Version = $version
}
}
return ($output | Sort-Object Name)
}
function Get-PipxVersion {
$result = (Get-CommandResult "pipx --version").Output
$result -match "(?<version>\d+\.\d+\.\d+\.?\d*)" | Out-Null
$pipxVersion = $Matches.Version
return "Pipx $pipxVersion"
}
function Get-GraalVMVersion {
$version = & "$env:GRAALVM_11_ROOT\bin\java" --version | Select-String -Pattern "GraalVM" | Take-OutputPart -Part 5,6
return $version
}
function Build-GraalVMTable {
$version = Get-GraalVMVersion
$envVariables = "GRAALVM_11_ROOT"
return [PSCustomObject] @{
"Version" = $version
"Environment variables" = $envVariables
}
}
function Build-PackageManagementEnvironmentTable {
return @(
@{
"Name" = "CONDA"
"Value" = $env:CONDA
},
@{
"Name" = "VCPKG_INSTALLATION_ROOT"
"Value" = $env:VCPKG_INSTALLATION_ROOT
}
) | ForEach-Object {
[PSCustomObject] @{
"Name" = $_.Name
"Value" = $_.Value
}
}
}
@@ -0,0 +1,76 @@
function Get-PostgreSqlVersion {
$postgreSQLVersion = psql --version | Take-OutputPart -Part 2
$aptSourceRepo = Get-AptSourceRepository -PackageName "postgresql"
return "PostgreSQL $postgreSQLVersion (apt source repository: $aptSourceRepo)"
}
function Get-MongoDbVersion {
$mongoDBVersion = mongod --version | Select-Object -First 1 | Take-OutputPart -Part 2 -Delimiter "v"
$aptSourceRepo = Get-AptSourceRepository -PackageName "mongodb"
return "MongoDB $mongoDBVersion (apt source repository: $aptSourceRepo)"
}
function Get-SqliteVersion {
$sqliteVersion = sqlite3 --version | Take-OutputPart -Part 0
return "sqlite3 $sqliteVersion"
}
function Get-MySQLVersion {
$mySQLVersion = mysqld --version | Take-OutputPart -Part 2
if (-not (Test-IsUbuntu20)) {
$mySQLVersion = $mySQLVersion | Take-OutputPart -Part 0 -Delimiter "-"
}
return "MySQL $mySQLVersion"
}
function Get-SQLCmdVersion {
$sqlcmdVersion = sqlcmd -? | Select-String -Pattern "Version" | Take-OutputPart -Part 1
return "sqlcmd $sqlcmdVersion"
}
function Get-SqlPackageVersion {
$sqlPackageVersion = sqlpackage /version
return "SqlPackage $sqlPackageVersion"
}
function Build-PostgreSqlSection {
$output = ""
$output += New-MDHeader "PostgreSQL" -Level 4
$output += New-MDList -Style Unordered -Lines @(
(Get-PostgreSqlVersion ),
"PostgreSQL Server (user:postgres)"
)
$output += New-MDCode -Lines @(
"PostgreSQL service is disabled by default. Use the following command as a part of your job to start the service: 'sudo systemctl start postgresql.service'"
)
return $output
}
function Build-MySQLSection {
$output = ""
$output += New-MDHeader "MySQL" -Level 4
$output += New-MDList -Style Unordered -Lines @(
(Get-MySQLVersion ),
"MySQL Server (user:root password:root)"
)
$output += New-MDCode -Lines @(
"MySQL service is disabled by default. Use the following command as a part of your job to start the service: 'sudo systemctl start mysql.service'"
)
return $output
}
function Build-MSSQLToolsSection {
$output = ""
$output += New-MDHeader "MS SQL Server Client Tools" -Level 4
$output += New-MDList -Style Unordered -Lines @(
(Get-SQLCmdVersion),
(Get-SqlPackageVersion)
)
return $output
}
@@ -0,0 +1,283 @@
param (
[Parameter(Mandatory)][string]
$OutputDirectory
)
$global:ErrorActionPreference = "Stop"
$global:ErrorView = "NormalView"
Set-StrictMode -Version Latest
Import-Module MarkdownPS
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.Android.psm1") -DisableNameChecking
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.Browsers.psm1") -DisableNameChecking
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.CachedTools.psm1") -DisableNameChecking
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.Common.psm1") -DisableNameChecking
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.Databases.psm1") -DisableNameChecking
Import-Module "$PSScriptRoot/../helpers/SoftwareReport.Helpers.psm1" -DisableNameChecking
Import-Module "$PSScriptRoot/../helpers/Common.Helpers.psm1" -DisableNameChecking
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.Java.psm1") -DisableNameChecking
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.Rust.psm1") -DisableNameChecking
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.Tools.psm1") -DisableNameChecking
Import-Module (Join-Path $PSScriptRoot "SoftwareReport.WebServers.psm1") -DisableNameChecking
# Restore file owner in user profile
Restore-UserOwner
$markdown = ""
$OSName = Get-OSName
$markdown += New-MDHeader "$OSName" -Level 1
$kernelVersion = Get-KernelVersion
$markdown += New-MDList -Style Unordered -Lines @(
"$kernelVersion"
"Image Version: $env:IMAGE_VERSION"
)
$markdown += New-MDHeader "Installed Software" -Level 2
$markdown += New-MDHeader "Language and Runtime" -Level 3
$runtimesList = @(
(Get-BashVersion),
(Get-CPPVersions),
(Get-FortranVersions),
(Get-ErlangVersion),
(Get-ErlangRebar3Version),
(Get-MonoVersion),
(Get-MsbuildVersion),
(Get-NodeVersion),
(Get-PerlVersion),
(Get-PythonVersion),
(Get-Python3Version),
(Get-RubyVersion),
(Get-SwiftVersion),
(Get-JuliaVersion),
(Get-KotlinVersion),
(Get-ClangVersions),
(Get-ClangFormatVersions)
)
$markdown += New-MDList -Style Unordered -Lines ($runtimesList | Sort-Object)
$markdown += New-MDHeader "Package Management" -Level 3
$packageManagementList = @(
(Get-HomebrewVersion),
(Get-CpanVersion),
(Get-GemVersion),
(Get-MinicondaVersion),
(Get-HelmVersion),
(Get-NpmVersion),
(Get-YarnVersion),
(Get-PipxVersion),
(Get-PipVersion),
(Get-Pip3Version),
(Get-VcpkgVersion)
)
$markdown += New-MDList -Style Unordered -Lines ($packageManagementList | Sort-Object)
$markdown += New-MDHeader "Environment variables" -Level 4
$markdown += Build-PackageManagementEnvironmentTable | New-MDTable
$markdown += New-MDNewLine
$markdown += New-MDHeader "Project Management" -Level 3
$projectManagementList = @(
(Get-AntVersion),
(Get-GradleVersion),
(Get-MavenVersion),
(Get-SbtVersion)
)
if (Test-IsUbuntu20) {
$projectManagementList += @(
(Get-LernaVersion)
)
}
$markdown += New-MDList -Style Unordered -Lines ($projectManagementList | Sort-Object)
$markdown += New-MDHeader "Tools" -Level 3
$toolsList = @(
(Get-AnsibleVersion),
(Get-AptFastVersion),
(Get-AzCopyVersion),
(Get-BazelVersion),
(Get-BazeliskVersion),
(Get-BicepVersion),
(Get-BuildahVersion),
(Get-CodeQLBundleVersion),
(Get-CMakeVersion),
(Get-DockerMobyClientVersion),
(Get-DockerMobyServerVersion),
(Get-DockerComposeV1Version),
(Get-DockerComposeV2Version),
(Get-DockerBuildxVersion),
(Get-GitVersion),
(Get-GitLFSVersion),
(Get-GitFTPVersion),
(Get-HavegedVersion),
(Get-HerokuVersion),
(Get-HHVMVersion),
(Get-SVNVersion),
(Get-JqVersion),
(Get-YqVersion),
(Get-KindVersion),
(Get-KubectlVersion),
(Get-KustomizeVersion),
(Get-LeiningenVersion),
(Get-MediainfoVersion),
(Get-HGVersion),
(Get-MinikubeVersion),
(Get-NewmanVersion),
(Get-NVersion),
(Get-NvmVersion),
(Get-OpensslVersion),
(Get-PackerVersion),
(Get-ParcelVersion),
(Get-PhantomJSVersion),
(Get-PodManVersion),
(Get-PulumiVersion),
(Get-RVersion),
(Get-SkopeoVersion),
(Get-SphinxVersion),
(Get-TerraformVersion),
(Get-YamllintVersion),
(Get-ZstdVersion)
)
if (Test-IsUbuntu20) {
$toolsList += (Get-FastlaneVersion)
}
$markdown += New-MDList -Style Unordered -Lines ($toolsList | Sort-Object)
$markdown += New-MDHeader "CLI Tools" -Level 3
$markdown += New-MDList -Style Unordered -Lines (@(
(Get-AlibabaCloudCliVersion),
(Get-AWSCliVersion),
(Get-AWSCliSessionManagerPluginVersion),
(Get-AWSSAMVersion),
(Get-AzureCliVersion),
(Get-AzureDevopsVersion),
(Get-GitHubCliVersion),
(Get-GoogleCloudSDKVersion),
(Get-HubCliVersion),
(Get-NetlifyCliVersion),
(Get-OCCliVersion),
(Get-ORASCliVersion),
(Get-VerselCliversion)
) | Sort-Object
)
$markdown += New-MDHeader "Java" -Level 3
$markdown += Get-JavaVersions | New-MDTable
$markdown += New-MDNewLine
if (Test-IsUbuntu20) {
$markdown += New-MDHeader "GraalVM" -Level 3
$markdown += Build-GraalVMTable | New-MDTable
$markdown += New-MDNewLine
}
$markdown += Build-PHPSection
$markdown += New-MDHeader "Haskell" -Level 3
$markdown += New-MDList -Style Unordered -Lines (@(
(Get-GHCVersion),
(Get-GHCupVersion),
(Get-CabalVersion),
(Get-StackVersion)
) | Sort-Object
)
$markdown += New-MDHeader "Rust Tools" -Level 3
$markdown += New-MDList -Style Unordered -Lines (@(
(Get-RustVersion),
(Get-RustupVersion),
(Get-RustdocVersion),
(Get-CargoVersion)
) | Sort-Object
)
$markdown += New-MDHeader "Packages" -Level 4
$markdown += New-MDList -Style Unordered -Lines (@(
(Get-BindgenVersion),
(Get-CargoAuditVersion),
(Get-CargoOutdatedVersion),
(Get-CargoClippyVersion),
(Get-CbindgenVersion),
(Get-RustfmtVersion)
) | Sort-Object
)
$markdown += New-MDHeader "Browsers and Drivers" -Level 3
$browsersAndDriversList = @(
(Get-ChromeVersion),
(Get-ChromeDriverVersion),
(Get-FirefoxVersion),
(Get-GeckodriverVersion),
(Get-ChromiumVersion),
(Get-SeleniumVersion)
)
$markdown += New-MDList -Style Unordered -Lines $browsersAndDriversList
$markdown += New-MDHeader "Environment variables" -Level 4
$markdown += Build-BrowserWebdriversEnvironmentTable | New-MDTable
$markdown += New-MDNewLine
$markdown += New-MDHeader ".NET Core SDK" -Level 3
$markdown += New-MDList -Style Unordered -Lines @(
(Get-DotNetCoreSdkVersions)
)
$markdown += New-MDHeader ".NET tools" -Level 3
$tools = Get-DotnetTools
$markdown += New-MDList -Lines $tools -Style Unordered
$markdown += New-MDHeader "Databases" -Level 3
$markdown += New-MDList -Style Unordered -Lines (@(
(Get-MongoDbVersion),
(Get-SqliteVersion)
) | Sort-Object
)
$markdown += Build-PostgreSqlSection
$markdown += Build-MySQLSection
$markdown += Build-MSSQLToolsSection
$markdown += New-MDHeader "Cached Tools" -Level 3
$markdown += Build-CachedToolsSection
$markdown += New-MDHeader "Environment variables" -Level 4
$markdown += Build-GoEnvironmentTable | New-MDTable
$markdown += New-MDNewLine
$markdown += New-MDHeader "PowerShell Tools" -Level 3
$markdown += New-MDList -Lines (Get-PowershellVersion) -Style Unordered
$markdown += New-MDHeader "PowerShell Modules" -Level 4
$markdown += Get-PowerShellModules | New-MDTable
$markdown += New-MDNewLine
$markdown += New-MDHeader "Az PowerShell Modules" -Level 4
$markdown += New-MDList -Style Unordered -Lines @(
(Get-AzModuleVersions)
)
$markdown += Build-WebServersSection
$markdown += New-MDHeader "Android" -Level 3
$markdown += Build-AndroidTable | New-MDTable
$markdown += New-MDNewLine
$markdown += New-MDHeader "Environment variables" -Level 4
$markdown += Build-AndroidEnvironmentTable | New-MDTable
$markdown += New-MDNewLine
$markdown += New-MDHeader "Cached Docker images" -Level 3
$markdown += Get-CachedDockerImagesTableData | New-MDTable
$markdown += New-MDNewLine
$markdown += New-MDHeader "Installed apt packages" -Level 3
$markdown += Get-AptPackages | New-MDTable
Test-BlankElement
$markdown | Out-File -FilePath "${OutputDirectory}/Ubuntu-Readme.md"
@@ -0,0 +1,22 @@
function Get-JavaVersions {
$javaToolcacheVersions = Get-ChildItem $env:AGENT_TOOLSDIRECTORY/Java*/* -Directory | Sort-Object { [int]$_.Name.Split(".")[0] }
$existingVersions = $javaToolcacheVersions | ForEach-Object {
$majorVersion = $_.Name.split(".")[0]
$fullVersion = $_.Name.Replace("-", "+")
$defaultJavaPath = $env:JAVA_HOME
$javaPath = Get-Item env:JAVA_HOME_${majorVersion}_X64
$defaultPostfix = ($javaPath.Value -eq $defaultJavaPath) ? " (default)" : ""
$vendorName = ($_.FullName -like '*Java_Adopt_jdk*') ? "Adopt OpenJDK" : "Eclipse Temurin"
[PSCustomObject] @{
"Version" = $fullVersion + $defaultPostfix
"Vendor" = $vendorName
"Environment Variable" = $javaPath.Name
}
}
# Return all the vendors which are not Adopt, also look for version 12 of Adopt (Eclipse Temurin does not have this version)
$versionsToReturn = $existingVersions | Where-Object {$_.Vendor -notlike "Adopt*" -or $_.Version.Split(".")[0] -eq 12}
return $versionsToReturn
}
@@ -0,0 +1,56 @@
function Initialize-RustEnvironment {
$env:PATH = "/etc/skel/.cargo/bin:/etc/skel/.rustup/bin:$($env:PATH)"
$env:RUSTUP_HOME = "/etc/skel/.rustup"
$env:CARGO_HOME = "/etc/skel/.cargo"
}
function Get-RustVersion {
Initialize-RustEnvironment
$rustVersion = $(rustc --version) | Take-OutputPart -Part 1
return "Rust $rustVersion"
}
function Get-BindgenVersion {
$bindgenVersion = $(bindgen --version) | Take-OutputPart -Part 1
return "Bindgen $bindgenVersion"
}
function Get-CargoVersion {
$cargoVersion = $(cargo --version) | Take-OutputPart -Part 1
return "Cargo $cargoVersion"
}
function Get-CargoAuditVersion {
$cargoAuditVersion = $(cargo audit --version) | Take-OutputPart -Part 1
return "Cargo audit $cargoAuditVersion"
}
function Get-CargoOutdatedVersion {
$cargoOutdatedVersion = $(cargo outdated --version) | Take-OutputPart -Part 1 -Delimiter "v"
return "Cargo outdated $cargoOutdatedVersion"
}
function Get-CargoClippyVersion {
$cargoClippyVersion = $(cargo-clippy --version) | Take-OutputPart -Part 1
return "Cargo clippy $cargoClippyVersion"
}
function Get-CbindgenVersion {
$cbindgenVersion = $(cbindgen --version) | Take-OutputPart -Part 1
return "Cbindgen $cbindgenVersion"
}
function Get-RustupVersion {
$rustupVersion = $(rustup --version) | Take-OutputPart -Part 1
return "Rustup $rustupVersion"
}
function Get-RustdocVersion {
$rustdocVersion = $(rustdoc --version) | Take-OutputPart -Part 1
return "Rustdoc $rustdocVersion"
}
function Get-RustfmtVersion {
$rustfmtVersion = $(rustfmt --version) | Take-OutputPart -Part 1 | Take-OutputPart -Part 0 -Delimiter "-"
return "Rustfmt $rustfmtVersion"
}
@@ -0,0 +1,294 @@
function Get-AnsibleVersion {
$ansibleVersion = (ansible --version)[0] -replace "[^\d.]"
return "Ansible $ansibleVersion"
}
function Get-AptFastVersion {
$versionFileContent = Get-Content (which apt-fast) -Raw
$match = [Regex]::Match($versionFileContent, '# apt-fast v(.+)\n')
$aptFastVersion = $match.Groups[1].Value
return "apt-fast $aptFastVersion"
}
function Get-AzCopyVersion {
$azcopyVersion = azcopy --version | Take-OutputPart -Part 2
return "AzCopy $azcopyVersion (available by ``azcopy`` and ``azcopy10`` aliases)"
}
function Get-BazelVersion {
$bazelVersion = bazel --version | Select-String "bazel" | Take-OutputPart -Part 1
return "Bazel $bazelVersion"
}
function Get-BazeliskVersion {
$result = Get-CommandResult "bazelisk version" -Multiline
$bazeliskVersion = $result.Output | Select-String "Bazelisk version:" | Take-OutputPart -Part 2 | Take-OutputPart -Part 0 -Delimiter "v"
return "Bazelisk $bazeliskVersion"
}
function Get-BicepVersion {
(bicep --version | Out-String) -match "bicep cli version (?<version>\d+\.\d+\.\d+)" | Out-Null
$bicepVersion = $Matches.Version
return "Bicep $bicepVersion"
}
function Get-CodeQLBundleVersion {
$CodeQLVersionsWildcard = Join-Path $Env:AGENT_TOOLSDIRECTORY -ChildPath "CodeQL" | Join-Path -ChildPath "*"
$CodeQLVersionPath = Get-ChildItem $CodeQLVersionsWildcard | Select-Object -First 1 -Expand FullName
$CodeQLPath = Join-Path $CodeQLVersionPath -ChildPath "x64" | Join-Path -ChildPath "codeql" | Join-Path -ChildPath "codeql"
$CodeQLVersion = & $CodeQLPath version --quiet
return "CodeQL Action Bundle $CodeQLVersion"
}
function Get-PodManVersion {
$podmanVersion = podman --version | Take-OutputPart -Part 2
$aptSourceRepo = Get-AptSourceRepository -PackageName "containers"
return "Podman $podmanVersion (apt source repository: $aptSourceRepo)"
}
function Get-BuildahVersion {
$buildahVersion = buildah --version | Take-OutputPart -Part 2
$aptSourceRepo = Get-AptSourceRepository -PackageName "containers"
return "Buildah $buildahVersion (apt source repository: $aptSourceRepo)"
}
function Get-SkopeoVersion {
$skopeoVersion = skopeo --version | Take-OutputPart -Part 2
$aptSourceRepo = Get-AptSourceRepository -PackageName "containers"
return "Skopeo $skopeoVersion (apt source repository: $aptSourceRepo)"
}
function Get-CMakeVersion {
$cmakeVersion = cmake --version | Select-Object -First 1 | Take-OutputPart -Part 2
return "CMake $cmakeVersion"
}
function Get-DockerComposeV1Version {
$composeVersion = docker-compose -v | Take-OutputPart -Part 2 | Take-OutputPart -Part 0 -Delimiter ","
return "Docker Compose v1 $composeVersion"
}
function Get-DockerComposeV2Version {
$composeVersion = docker compose version | Take-OutputPart -Part 3
return "Docker Compose v2 $composeVersion"
}
function Get-DockerMobyClientVersion {
$dockerClientVersion = sudo docker version --format '{{.Client.Version}}'
return "Docker-Moby Client $dockerClientVersion"
}
function Get-DockerMobyServerVersion {
$dockerServerVersion = sudo docker version --format '{{.Server.Version}}'
return "Docker-Moby Server $dockerServerVersion"
}
function Get-DockerBuildxVersion {
$buildxVersion = docker buildx version | Take-OutputPart -Part 1 | Take-OutputPart -Part 0 -Delimiter "+"
return "Docker-Buildx $buildxVersion"
}
function Get-GitVersion {
$gitVersion = git --version | Take-OutputPart -Part -1
$aptSourceRepo = Get-AptSourceRepository -PackageName "git-core"
return "Git $gitVersion (apt source repository: $aptSourceRepo)"
}
function Get-GitLFSVersion {
$result = Get-CommandResult "git-lfs --version"
$gitlfsversion = $result.Output | Take-OutputPart -Part 0 | Take-OutputPart -Part 1 -Delimiter "/"
$aptSourceRepo = Get-AptSourceRepository -PackageName "git-lfs"
return "Git LFS $gitlfsversion (apt source repository: $aptSourceRepo)"
}
function Get-GitFTPVersion {
$gitftpVersion = git-ftp --version | Take-OutputPart -Part 2
return "Git-ftp $gitftpVersion"
}
function Get-GoogleCloudSDKVersion {
$aptSourceRepo = Get-AptSourceRepository -PackageName "google-cloud-sdk"
return "$(gcloud --version | Select-Object -First 1) (apt source repository: $aptSourceRepo)"
}
function Get-HavegedVersion {
$havegedVersion = dpkg-query --showformat='${Version}' --show haveged | Take-OutputPart -Part 0 -Delimiter "-"
return "Haveged $havegedVersion"
}
function Get-HerokuVersion {
$herokuVersion = heroku version | Take-OutputPart -Part 0 | Take-OutputPart -Part 1 -Delimiter "/"
return "Heroku $herokuVersion"
}
function Get-HHVMVersion {
$hhvmVersion = hhvm --version | Select-Object -First 1 | Take-OutputPart -Part 2
return "HHVM (HipHop VM) $hhvmVersion"
}
function Get-SVNVersion {
$svnVersion = svn --version | Select-Object -First 1 | Take-OutputPart -Part 2
return "SVN $svnVersion"
}
function Get-KustomizeVersion {
$kustomizeVersion = kustomize version --short | Take-OutputPart -Part 0 | Take-OutputPart -Part 1 -Delimiter "v"
return "Kustomize $kustomizeVersion"
}
function Get-KindVersion {
$kindVersion = kind version | Take-OutputPart -Part 1 | Take-OutputPart -Part 0 -Delimiter "v"
return "Kind $kindVersion"
}
function Get-KubectlVersion {
$kubectlVersion = kubectl version --client --short | Take-OutputPart -Part 2 | Take-OutputPart -Part 0 -Delimiter "v"
return "Kubectl $kubectlVersion"
}
function Get-MinikubeVersion {
$minikubeVersion = minikube version --short | Take-OutputPart -Part 0 -Delimiter "v"
return "Minikube $minikubeVersion"
}
function Get-HGVersion {
$hgVersion = hg --version | Select-Object -First 1 | Take-OutputPart -Part -1 | Take-OutputPart -Part 0 -Delimiter ")"
return "Mercurial $hgVersion"
}
function Get-LeiningenVersion {
return "$(lein -v | Take-OutputPart -Part 0,1)"
}
function Get-MediainfoVersion {
$mediainfoVersion = (mediainfo --version | Select-Object -Index 1 | Take-OutputPart -Part 2).Replace('v', '')
return "MediaInfo $mediainfoVersion"
}
function Get-NewmanVersion {
return "Newman $(newman --version)"
}
function Get-NVersion {
$nVersion = (n --version).Replace('v', '')
return "n $nVersion"
}
function Get-NvmVersion {
$nvmVersion = bash -c "source /etc/skel/.nvm/nvm.sh && nvm --version"
return "nvm $nvmVersion"
}
function Get-PackerVersion {
# Packer 1.7.1 has a bug and outputs version to stderr instead of stdout https://github.com/hashicorp/packer/issues/10855
$result = (Get-CommandResult "packer --version").Output
$packerVersion = [regex]::matches($result, "(\d+.){2}\d+").Value
return "Packer $packerVersion"
}
function Get-PhantomJSVersion {
return "PhantomJS $(phantomjs --version)"
}
function Get-TerraformVersion {
return (terraform version | Select-String "^Terraform").Line.Replace('v','')
}
function Get-JqVersion {
$jqVersion = jq --version | Take-OutputPart -Part 1 -Delimiter "-"
return "jq $jqVersion"
}
function Get-AzureCliVersion {
$azcliVersion = (az version | ConvertFrom-Json).'azure-cli'
$aptSourceRepo = Get-AptSourceRepository -PackageName "azure-cli"
return "Azure CLI (azure-cli) $azcliVersion (installation method: $aptSourceRepo)"
}
function Get-AzureDevopsVersion {
$azdevopsVersion = (az version | ConvertFrom-Json).extensions.'azure-devops'
return "Azure CLI (azure-devops) $azdevopsVersion"
}
function Get-AlibabaCloudCliVersion {
return "Alibaba Cloud CLI $(aliyun version)"
}
function Get-AWSCliVersion {
$result = Get-CommandResult "aws --version"
$awsVersion = $result.Output | Take-OutputPart -Part 0 | Take-OutputPart -Part 1 -Delimiter "/"
return "AWS CLI $awsVersion"
}
function Get-AWSCliSessionManagerPluginVersion {
$result = (Get-CommandResult "session-manager-plugin --version").Output
return "AWS CLI Session manager plugin $result"
}
function Get-AWSSAMVersion {
return "AWS SAM CLI $(sam --version | Take-OutputPart -Part -1)"
}
function Get-FastlaneVersion {
$fastlaneVersion = fastlane --version | Select-String "^fastlane [0-9]" | Take-OutputPart -Part 1
return "Fastlane $fastlaneVersion"
}
function Get-HubCliVersion {
$hubVersion = hub --version | Select-String "hub version" | Take-OutputPart -Part 2
return "Hub CLI $hubVersion"
}
function Get-GitHubCliVersion {
$ghVersion = gh --version | Select-String "gh version" | Take-OutputPart -Part 2
return "GitHub CLI $ghVersion"
}
function Get-NetlifyCliVersion {
$netlifyVersion = netlify --version | Take-OutputPart -Part 0 | Take-OutputPart -Part 1 -Delimiter "/"
return "Netlify CLI $netlifyVersion"
}
function Get-OCCliVersion {
$ocVersion = oc version | Take-OutputPart -Part 2 | Take-OutputPart -Part 0 -Delimiter "-"
return "OpenShift CLI $ocVersion"
}
function Get-ORASCliVersion {
$orasVersion = oras version | Select-String "^Version:" | Take-OutputPart -Part 1
return "ORAS CLI $orasVersion"
}
function Get-VerselCliversion {
$result = Get-CommandResult "vercel --version" -Multiline
return $result.Output | Select-Object -First 1
}
function Get-PulumiVersion {
$pulumiVersion = pulumi version | Take-OutputPart -Part 0 -Delimiter "v"
return "Pulumi $pulumiVersion"
}
function Get-RVersion {
$rVersion = (Get-CommandResult "R --version | grep 'R version'").Output | Take-OutputPart -Part 2
return "R $rVersion"
}
function Get-SphinxVersion {
$sphinxVersion = searchd -h | Select-Object -First 1 | Take-OutputPart -Part 1 | Take-OutputPart -Part 0 -Delimiter "-"
return "Sphinx Open Source Search Server $sphinxVersion"
}
function Get-YamllintVersion {
return "$(yamllint --version)"
}
function Get-ZstdVersion {
$zstdVersion = zstd --version | Take-OutputPart -Part 1 -Delimiter "v" | Take-OutputPart -Part 0 -Delimiter ","
return "zstd $zstdVersion (homebrew)"
}
function Get-YqVersion {
$yqVersion = ($(yq -V) -Split " ")[-1]
return "yq $yqVersion"
}
@@ -0,0 +1,61 @@
function Get-ApacheVersion {
$name = "apache2"
$port = 80
$version = bash -c "apache2 -v | grep -Po 'Apache/(\d+.){2}\d+'" | Take-OutputPart -Part 1 -Delimiter "/"
$serviceStatus = systemctl status apache2 | grep "Active:" | Take-OutputPart -Part 1
$configFile = "/etc/apache2/apache2.conf"
return [PsCustomObject]@{
"Name" = $name
"Version" = $version
"ConfigFile" = $configFile
"ServiceStatus" = $serviceStatus
"ListenPort" = $port
}
}
function Get-NginxVersion {
$name = "nginx"
$port = 80
$version = (dpkg-query --showformat='${Version}' --show nginx).Split('-')[0]
$serviceStatus = systemctl status nginx | grep "Active:" | Take-OutputPart -Part 1
$configFile = "/etc/nginx/nginx.conf"
return [PsCustomObject]@{
"Name" = $name
"Version" = $version
"ConfigFile" = $configFile
"ServiceStatus" = $serviceStatus
"ListenPort" = $port
}
}
function Get-Xsp4Version {
$name = "mono-xsp4"
$port = (grep '^port=' /etc/default/mono-xsp4).Split('=')[1]
$version = (dpkg-query --showformat='${Version}' --show mono-xsp4).Split('-')[0]
$serviceStatus = systemctl show -p ActiveState --value mono-xsp4
$configFile = "/etc/default/mono-xsp4"
return [PsCustomObject]@{
"Name" = $name
"Version" = $version
"ConfigFile" = $configFile
"ServiceStatus" = $serviceStatus
"ListenPort" = $port
}
}
function Build-WebServersSection {
$servers = @(
(Get-ApacheVersion),
(Get-NginxVersion)
)
if (Test-IsUbuntu20) {
$servers += (Get-Xsp4Version)
}
$output = ""
$output += New-MDHeader "Web Servers" -Level 3
$output += $servers | Sort-Object Name | New-MDTable
$output += New-MDNewLine
return $output
}
@@ -0,0 +1,7 @@
#!/bin/bash -e
prefix=/usr/local/bin
for tool in apt apt-get apt-fast apt-key;do
sudo rm -f $prefix/$tool
done
@@ -1,15 +1,12 @@
#!/bin/bash -e
################################################################################
## File: configure-apt-mock.sh
## Desc: A temporary workaround for https://github.com/Azure/azure-linux-extensions/issues/1238.
## Cleaned up during cleanup.sh.
################################################################################
# A temporary workaround for https://github.com/Azure/azure-linux-extensions/issues/1238
prefix=/usr/local/bin
for real_tool in /usr/bin/apt /usr/bin/apt-get /usr/bin/apt-key; do
tool=$(basename $real_tool)
cat >$prefix/$tool <<EOT
for real_tool in /usr/bin/apt /usr/bin/apt-get /usr/bin/apt-fast /usr/bin/apt-key;do
tool=`basename $real_tool`
cat >$prefix/$tool <<EOT
#!/bin/sh
i=1
@@ -50,5 +47,5 @@ while [ \$i -le 30 ];do
i=\$((i + 1))
done
EOT
chmod +x $prefix/$tool
chmod +x $prefix/$tool
done
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash -e
# Stop and disable apt-daily upgrade services;
systemctl stop apt-daily.timer
systemctl disable apt-daily.timer
systemctl disable apt-daily.service
systemctl stop apt-daily-upgrade.timer
systemctl disable apt-daily-upgrade.timer
systemctl disable apt-daily-upgrade.service
# Enable retry logic for apt up to 10 times
echo "APT::Acquire::Retries \"10\";" > /etc/apt/apt.conf.d/80-retries
# Configure apt to always assume Y
echo "APT::Get::Assume-Yes \"true\";" > /etc/apt/apt.conf.d/90assumeyes
# Fix bad proxy and http headers settings
cat <<EOF >> /etc/apt/apt.conf.d/99bad_proxy
Acquire::http::Pipeline-Depth 0;
Acquire::http::No-Cache true;
Acquire::BrokenProxy true;
EOF
# Uninstall unattended-upgrades
apt-get purge unattended-upgrades
# Need to limit arch for default apt repos due to
# https://github.com/actions/virtual-environments/issues/1961
sed -i'' -E 's/^deb http:\/\/(azure.archive|security).ubuntu.com/deb [arch=amd64,i386] http:\/\/\1.ubuntu.com/' /etc/apt/sources.list
echo 'APT sources limited to the actual architectures'
cat /etc/apt/sources.list
apt-get update
# Install jq
apt-get install jq
# Install apt-fast using quick-install.sh
# https://github.com/ilikenwf/apt-fast
bash -c "$(curl -sL https://raw.githubusercontent.com/ilikenwf/apt-fast/master/quick-install.sh)"
@@ -1,8 +1,4 @@
#!/bin/bash -e
################################################################################
## File: configure-limits.sh
## Desc: Configure limits
################################################################################
echo 'session required pam_limits.so' >> /etc/pam.d/common-session
echo 'session required pam_limits.so' >> /etc/pam.d/common-session-noninteractive
@@ -1,6 +1,8 @@
#!/bin/bash -e
################################################################################
## File: Install-TortoiseSvn.ps1
## Desc: Install TortoiseSvn
## File: reboot.sh
## Desc: Reboot VM
################################################################################
Install-ChocoPackage tortoisesvn
echo "Reboot VM"
sudo reboot

Some files were not shown because too many files have changed in this diff Show More