Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions .github/scripts/prepare-vsix.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $Sources,
[Parameter(Mandatory = $true)]
[string] $OutputDirectory
)

$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$platformIds = @("win32-x64", "linux-x64", "darwin-arm64")
$urls = @($Sources -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } | Select-Object -Unique)
if ($urls.Count -eq 0) {
throw "No VSIX sources supplied"
}
if (Test-Path -LiteralPath $OutputDirectory) {
throw "VSIX output directory already exists: $OutputDirectory"
}

$downloadCache = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::Ordinal)
$runCache = @{}
$cacheDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "java-pack-vsix-$([guid]::NewGuid())"
New-Item -ItemType Directory -Path $cacheDirectory | Out-Null

function Get-GitHubHeaders {
param([switch] $Actions)

$token = if ($Actions -and $env:VSCODE_JAVA_ARTIFACT_TOKEN) { $env:VSCODE_JAVA_ARTIFACT_TOKEN } else { $env:GITHUB_TOKEN }
if (-not $token) {
throw "GitHub API requests require GITHUB_TOKEN or, for Actions artifacts, VSCODE_JAVA_ARTIFACT_TOKEN"
}
return @{
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
"User-Agent" = "vscode-java-pack-autotest"
Authorization = "Bearer $token"
}
}

function Select-VsixForPlatform {
param(
[AllowEmptyCollection()]
[object[]] $VsixFiles,
[string] $PlatformId
)

$platformVsix = $VsixFiles | Where-Object { $_.Name -like "*-$PlatformId-*" } | Sort-Object Name | Select-Object -First 1
if ($platformVsix) {
return $platformVsix
}
$universalVsix = $VsixFiles | Where-Object { $_.Name -notmatch '-(darwin|linux|win32)-' } | Sort-Object Name | Select-Object -First 1
if ($universalVsix) {
return $universalVsix
}
throw "No matching VSIX found for platform $PlatformId. Available VSIX files: $($VsixFiles.Name -join ', ')"
}

function Get-DownloadedVsix {
param([string] $Url)

if (-not $downloadCache.ContainsKey($Url)) {
$uri = [uri] $Url
if (-not $uri.IsAbsoluteUri -or $uri.Scheme -notin @("https", "http")) {
throw "VSIX downloads require an absolute HTTP(S) URL"
}
$fileName = [System.IO.Path]::GetFileName([uri]::UnescapeDataString($uri.AbsolutePath))
if (-not $fileName.EndsWith(".vsix", [System.StringComparison]::OrdinalIgnoreCase)) {
throw "VSIX download URL must name a .vsix file"
}
$directory = Join-Path $cacheDirectory "download-$($downloadCache.Count)"
New-Item -ItemType Directory -Path $directory | Out-Null
$filePath = Join-Path $directory $fileName
Write-Host "Downloading VSIX: $fileName"
# Direct and release-asset URLs must not receive GitHub API credentials.
Invoke-WebRequest -Uri $uri -OutFile $filePath -UseBasicParsing | Out-Null
$downloadCache.Add($Url, $filePath)
}
return Get-Item -LiteralPath $downloadCache[$Url]
}

function Get-GitHubActionsRunVsix {
param([string] $Owner, [string] $Repo, [string] $RunId)

$key = "$Owner/$Repo/$RunId"
if (-not $runCache.ContainsKey($key)) {
Write-Host "Resolving GitHub Actions run: $Owner/$Repo#$RunId"
$headers = Get-GitHubHeaders -Actions
$artifactsUrl = "https://api.github.com/repos/$Owner/$Repo/actions/runs/$RunId/artifacts?per_page=100"
$artifacts = @((Invoke-RestMethod -Uri $artifactsUrl -Headers $headers -UseBasicParsing).artifacts | Where-Object { -not $_.expired })
if ($artifacts.Count -eq 0) {
throw "No non-expired artifacts found for GitHub Actions run $Owner/$Repo#$RunId"
}
$artifact = $artifacts | Where-Object { $_.name -eq "vscode-java" } | Select-Object -First 1
if (-not $artifact -and $artifacts.Count -eq 1) {
$artifact = $artifacts[0]
}
if (-not $artifact) {
throw "No vscode-java artifact found for GitHub Actions run $Owner/$Repo#$RunId. Available artifacts: $($artifacts.name -join ', ')"
}
$archiveUri = [uri] $artifact.archive_download_url
if ($archiveUri.Scheme -ne "https" -or $archiveUri.Host -ne "api.github.com") {
throw "GitHub artifact archive URL must use https://api.github.com"
}
$directory = Join-Path $cacheDirectory "run-$($runCache.Count)"
$zipPath = "$directory.zip"
Write-Host "Downloading Actions artifact: $($artifact.name)"
try {
Invoke-WebRequest -Uri $archiveUri -OutFile $zipPath -Headers $headers -UseBasicParsing | Out-Null
} catch {
throw "Failed to download artifact $($artifact.name) from $Owner/$Repo#$RunId. Cross-repo downloads may require VSCODE_JAVA_ARTIFACT_TOKEN with actions:read access. $($_.Exception.Message)"
}
Expand-Archive -LiteralPath $zipPath -DestinationPath $directory -Force
$files = @(Get-ChildItem -LiteralPath $directory -Recurse -File -Filter "*.vsix")
if ($files.Count -eq 0) {
throw "Artifact $($artifact.name) from $Owner/$Repo#$RunId does not contain VSIX files"
}
$runCache[$key] = $files
}
return $runCache[$key]
}

try {
foreach ($platformId in $platformIds) {
New-Item -ItemType Directory -Path (Join-Path $OutputDirectory $platformId) -Force | Out-Null
}
foreach ($url in $urls) {
if ($url -match '^https://github\.com/([^/]+)/([^/]+)/releases/tag/(.+)$') {
$owner = $Matches[1]; $repo = $Matches[2]; $tag = $Matches[3]
Write-Host "Resolving GitHub release: $owner/$repo@$tag"
$apiUrl = "https://api.github.com/repos/$owner/$repo/releases/tags/$tag"
$release = Invoke-RestMethod -Uri $apiUrl -Headers (Get-GitHubHeaders) -UseBasicParsing
$assets = @($release.assets | Where-Object { $_.name -like "*.vsix" })
foreach ($platformId in $platformIds) {
$asset = Select-VsixForPlatform -VsixFiles $assets -PlatformId $platformId
$file = Get-DownloadedVsix -Url $asset.browser_download_url
Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $OutputDirectory "$platformId/$($file.Name)") -Force
}
} elseif ($url -match '^https://github\.com/([^/]+)/([^/]+)/actions/runs/(\d+)') {
$files = @(Get-GitHubActionsRunVsix -Owner $Matches[1] -Repo $Matches[2] -RunId $Matches[3])
foreach ($platformId in $platformIds) {
$file = Select-VsixForPlatform -VsixFiles $files -PlatformId $platformId
Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $OutputDirectory "$platformId/$($file.Name)") -Force
}
} elseif ($url -match '^\d+$') {
$files = @(Get-GitHubActionsRunVsix -Owner "redhat-developer" -Repo "vscode-java" -RunId $url)
foreach ($platformId in $platformIds) {
$file = Select-VsixForPlatform -VsixFiles $files -PlatformId $platformId
Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $OutputDirectory "$platformId/$($file.Name)") -Force
}
} else {
$file = Get-DownloadedVsix -Url $url
foreach ($platformId in $platformIds) {
Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $OutputDirectory "$platformId/$($file.Name)") -Force
}
}
}
Get-ChildItem -LiteralPath $OutputDirectory -Recurse -File -Filter "*.vsix" | ForEach-Object {
Write-Host "Prepared $($_.Directory.Name): $($_.Name) ($([math]::Round($_.Length / 1MB, 1)) MB)"
}
} finally {
Remove-Item -LiteralPath $cacheDirectory -Recurse -Force
}
172 changes: 172 additions & 0 deletions .github/scripts/test-prepare-vsix.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
$ErrorActionPreference = "Stop"
$scriptPath = Join-Path $PSScriptRoot "prepare-vsix.ps1"
$testDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "java-pack-vsix-tests-$([guid]::NewGuid())"
$previousToken = $env:GITHUB_TOKEN
$previousArtifactToken = $env:VSCODE_JAVA_ARTIFACT_TOKEN
$platformIds = @("win32-x64", "linux-x64", "darwin-arm64")
$testState = @{
restCalls = [System.Collections.Generic.List[object]]::new()
webCalls = [System.Collections.Generic.List[object]]::new()
restFailure = ""
webFailure = ""
caseCount = 0
}
$releaseUrl = "https://github.com/redhat-developer/vscode-java/releases/tag/v1.57.0"
$runUrl = "https://github.com/redhat-developer/vscode-java/actions/runs/123"
$testState.archiveFiles = @($platformIds | ForEach-Object { "java-$_-1.57.0.vsix" })
$testState.assets = @($testState.archiveFiles | ForEach-Object {
[pscustomobject]@{ name = $_; browser_download_url = "https://downloads.example/$_" }
})
$testState.artifacts = @([pscustomobject]@{
name = "vscode-java"
expired = $false
archive_download_url = "https://api.github.com/repos/redhat-developer/vscode-java/actions/artifacts/1/zip"
})

function Assert-True {
param([bool] $Condition, [string] $Message)
if (-not $Condition) {
throw $Message
}
}

function Invoke-RestMethod {
param([string] $Uri, [hashtable] $Headers, [switch] $UseBasicParsing)
$testState.restCalls.Add([pscustomobject]@{ Uri = $Uri; Headers = $Headers })
if ($testState.restFailure) {
throw $testState.restFailure
}
if ($Uri -match '/releases/tags/') {
Assert-True ($Headers.Authorization -eq "Bearer $env:GITHUB_TOKEN") "Release metadata must use GITHUB_TOKEN"
return [pscustomobject]@{ assets = $testState.assets }
}
if ($Uri -match '/actions/runs/') {
$token = if ($env:VSCODE_JAVA_ARTIFACT_TOKEN) { $env:VSCODE_JAVA_ARTIFACT_TOKEN } else { $env:GITHUB_TOKEN }
Assert-True ($Headers.Authorization -eq "Bearer $token") "Actions metadata must use the artifact token or GITHUB_TOKEN"
return [pscustomobject]@{ artifacts = $testState.artifacts }
}
throw "Unexpected metadata request"
}

function Invoke-WebRequest {
param([uri] $Uri, [string] $OutFile, [hashtable] $Headers, [switch] $UseBasicParsing)
$testState.webCalls.Add([pscustomobject]@{ Uri = $Uri; Headers = $Headers })
if ($testState.webFailure) {
throw $testState.webFailure
}
if ($Uri.Host -eq "api.github.com") {
$token = if ($env:VSCODE_JAVA_ARTIFACT_TOKEN) { $env:VSCODE_JAVA_ARTIFACT_TOKEN } else { $env:GITHUB_TOKEN }
Assert-True ($Headers.Authorization -eq "Bearer $token") "Actions archive must use the artifact token or GITHUB_TOKEN"
} else {
Assert-True (-not $Headers) "Direct/release downloads must not receive API credentials"
}
[System.IO.File]::WriteAllText($OutFile, "fixture")
}

function Expand-Archive {
param([string] $LiteralPath, [string] $DestinationPath, [switch] $Force)
New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null
foreach ($name in $testState.archiveFiles) {
[System.IO.File]::WriteAllText((Join-Path $DestinationPath $name), "fixture")
}
}

function Invoke-TestPreparation {
param([string] $Sources, [string] $ExpectedError = "")

$testState.caseCount++
$testState.restCalls.Clear()
$testState.webCalls.Clear()
$output = Join-Path $testDirectory "case-$($testState.caseCount)"
try {
& $scriptPath -Sources $Sources -OutputDirectory $output 6> $null
} catch {
if (-not $ExpectedError -or $_.Exception.Message -notlike "*$ExpectedError*") {
throw
}
return $output
}
Assert-True (-not $ExpectedError) "Expected preparation failure: $ExpectedError"
return $output
}

function Assert-PlatformFiles {
param([string] $Directory, [switch] $Universal)
foreach ($platformId in $platformIds) {
$files = @(Get-ChildItem -LiteralPath (Join-Path $Directory $platformId) -File)
$expected = if ($Universal) { "extension-1.0.vsix" } else { "java-$platformId-1.57.0.vsix" }
Assert-True ($files.Count -eq 1 -and $files[0].Name -eq $expected) "Wrong bits for $platformId"
}
}

try {
New-Item -ItemType Directory -Path $testDirectory | Out-Null
$env:GITHUB_TOKEN = "test-github-token"
$env:VSCODE_JAVA_ARTIFACT_TOKEN = "test-artifact-token"

$testState.assets += [pscustomobject]@{ name = "java-linux-arm64-1.57.0.vsix"; browser_download_url = "https://downloads.example/wrong.vsix" }
$testState.assets += [pscustomobject]@{ name = "extension-1.0.vsix"; browser_download_url = "https://downloads.example/extension-1.0.vsix" }
$output = Invoke-TestPreparation "$releaseUrl, $releaseUrl"
Assert-PlatformFiles $output
Assert-True ($testState.restCalls.Count -eq 1 -and $testState.webCalls.Count -eq 3) "Release must resolve once and download only the three required platform assets"

$testState.assets = @([pscustomobject]@{ name = "extension-1.0.vsix"; browser_download_url = "https://downloads.example/extension-1.0.vsix" })
$output = Invoke-TestPreparation $releaseUrl
Assert-PlatformFiles $output -Universal
Assert-True ($testState.restCalls.Count -eq 1 -and $testState.webCalls.Count -eq 1) "Universal release must download once"

$output = Invoke-TestPreparation "$runUrl,123"
Assert-PlatformFiles $output
Assert-True ($testState.restCalls.Count -eq 1 -and $testState.webCalls.Count -eq 1) "Run URL and numeric alias must share one metadata/archive download"

$env:VSCODE_JAVA_ARTIFACT_TOKEN = $null
$output = Invoke-TestPreparation $runUrl
Assert-PlatformFiles $output
Assert-True ($testState.restCalls.Count -eq 1 -and $testState.webCalls.Count -eq 1) "Actions fallback token must retain single-download behavior"

$testState.artifacts[0].name = "sole-artifact"
$testState.archiveFiles = @("extension-1.0.vsix")
$output = Invoke-TestPreparation $runUrl
Assert-PlatformFiles $output -Universal

$env:GITHUB_TOKEN = $null
$output = Invoke-TestPreparation " https://downloads.example/extension-1.0.vsix?download=1,https://downloads.example/extension-1.0.vsix?download=1 "
Assert-PlatformFiles $output -Universal
Assert-True ($testState.restCalls.Count -eq 0 -and $testState.webCalls.Count -eq 1) "Direct URLs must download once without API authentication"

Invoke-TestPreparation $releaseUrl "GitHub API requests require" | Out-Null
Assert-True ($testState.restCalls.Count -eq 0) "Missing token must fail before calling the release API"
$env:GITHUB_TOKEN = "test-github-token"

$testState.assets = @([pscustomobject]@{ name = "java-win32-x64-1.57.0.vsix"; browser_download_url = "https://downloads.example/java-win32-x64-1.57.0.vsix" })
Invoke-TestPreparation $releaseUrl "No matching VSIX found for platform linux-x64" | Out-Null
$testState.assets = @()
Invoke-TestPreparation $releaseUrl "No matching VSIX found" | Out-Null

$testState.restFailure = "rate limit exceeded"
Invoke-TestPreparation $releaseUrl "rate limit exceeded" | Out-Null
Assert-True ($testState.webCalls.Count -eq 0) "Metadata failure must not fall back to other bits"
$testState.restFailure = ""

$testState.artifacts[0].expired = $true
Invoke-TestPreparation $runUrl "No non-expired artifacts" | Out-Null
$testState.artifacts[0].expired = $false
$testState.artifacts[0].archive_download_url = "https://downloads.example/archive.zip"
Invoke-TestPreparation $runUrl "archive URL must use https://api.github.com" | Out-Null
Assert-True ($testState.webCalls.Count -eq 0) "Archive credentials must not be sent to an external host"
$testState.artifacts[0].archive_download_url = "https://api.github.com/repos/redhat-developer/vscode-java/actions/artifacts/1/zip"

$testState.archiveFiles = @()
Invoke-TestPreparation $runUrl "does not contain VSIX files" | Out-Null
$testState.webFailure = "forbidden"
Invoke-TestPreparation $runUrl "Cross-repo downloads may require VSCODE_JAVA_ARTIFACT_TOKEN" | Out-Null
$testState.webFailure = ""

Invoke-TestPreparation "https://downloads.example/file.zip" "must name a .vsix file" | Out-Null
Invoke-TestPreparation " , " "No VSIX sources supplied" | Out-Null
Write-Host "Passed $($testState.caseCount) VSIX preparation tests."
} finally {
$env:GITHUB_TOKEN = $previousToken
$env:VSCODE_JAVA_ARTIFACT_TOKEN = $previousArtifactToken
Remove-Item -LiteralPath $testDirectory -Recurse -Force
}
3 changes: 3 additions & 0 deletions .github/skills/uitest/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,6 @@ The repository workflow `.github/workflows/e2e-autotest.yml` builds a branch VSI

Use workflow dispatch when the user asks to validate in CI or across platforms. The `test_plan` input can target a single plan; leave it empty to run all plans.

For manual runs with `vsix_urls`, `prepare-bits` resolves each release/run once, downloads the Windows x64, Linux x64, and macOS ARM64 VSIX files, and uploads platform artifacts. Matrix jobs reuse those bits alongside the branch-built `pack-vsix`; they do not query release APIs. Preparation or missing-platform failures block the matrix rather than silently testing Marketplace bits. Runs without supplied URLs skip preparation and keep the existing Marketplace behavior.

Release API requests use `GITHUB_TOKEN`. Cross-repository Actions artifacts use `VSCODE_JAVA_ARTIFACT_TOKEN` when configured, otherwise `GITHUB_TOKEN`; the token needs `actions:read` access to the source repository. Direct VSIX URLs are downloaded once and distributed unchanged, without GitHub API credentials.
Loading
Loading