Answers two things: how to install these without a marketplace, and how to keep settings in sync when VS Code's Settings Sync cannot be pointed at Gitea. - build.ps1 packages each extension into dist/ - publish.ps1 creates a release and uploads them, replacing same-named assets so a rolling "latest" tag stays clean - install.ps1 downloads a release's assets and installs them, or -Local from dist/ - settings.ps1 copies User/ between the repo and the machine, and installs the marketplace extensions recorded in extensions.txt - dev-link.ps1 junctions the sources into ~/.vscode/extensions for development - ci-release.sh plus a Gitea Actions workflow do the build and publish on a tag push Settings are copied, not symlinked: an atomic save replaces a symlink with a regular file and the sync stops without looking broken. vsce/ pins vsce and undici so packaging works on Node 18, which current vsce does not support. test/run.ps1 drives the real scripts against a stub of the Gitea release API. It caught the multipart Content-Disposition being unquoted on .NET Framework, and Invoke-WebRequest dropping the Authorization header across a redirect.
205 lines
8.3 KiB
PowerShell
205 lines
8.3 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Exercises publish.ps1, install.ps1 and ci-release.sh against a stub Gitea.
|
|
|
|
.DESCRIPTION
|
|
Runs the real scripts, pointed at test/fake-gitea.js instead of a server, with a
|
|
stub "code" on PATH so nothing is installed into the editor. What it checks:
|
|
|
|
- the token reaches the API, and the download after the redirect
|
|
- the .vsix arrives byte-for-byte (it is a zip, so any text encoding step shows up)
|
|
- republishing a tag replaces assets instead of duplicating them
|
|
- install passes --force, without which VS Code silently skips a same-version
|
|
reinstall
|
|
- build.ps1 and install.ps1 -Local work with no Gitea configured
|
|
- ci-release.sh agrees with publish.ps1
|
|
|
|
Needs dist/*.vsix, so it runs build.ps1 first unless -SkipBuild.
|
|
|
|
.EXAMPLE
|
|
.\test\run.ps1
|
|
.\test\run.ps1 -SkipBuild
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[int] $Port = 5602,
|
|
[switch] $SkipBuild
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$scripts = Join-Path $root 'scripts'
|
|
$dist = Join-Path $root 'dist'
|
|
$base = "http://127.0.0.1:$Port"
|
|
|
|
$failures = New-Object System.Collections.ArrayList
|
|
function Assert-That {
|
|
param([string] $What, [bool] $Condition, [string] $Detail = '')
|
|
if ($Condition) {
|
|
Write-Host " PASS $What" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " FAIL $What" -ForegroundColor Red
|
|
if ($Detail) { Write-Host " $Detail" -ForegroundColor DarkGray }
|
|
[void] $failures.Add($What)
|
|
}
|
|
}
|
|
|
|
function Get-State { Invoke-RestMethod -Uri "$base/__state" -UseBasicParsing }
|
|
|
|
# A stub code CLI, so an install never touches the real editor. It has to be found by
|
|
# Get-CodeCommand, which looks at PATH first.
|
|
$stubDir = Join-Path ([System.IO.Path]::GetTempPath()) "vscode-setup-test-$PID"
|
|
New-Item -ItemType Directory -Path $stubDir -Force | Out-Null
|
|
$callLog = Join-Path $stubDir 'calls.log'
|
|
Set-Content -Path (Join-Path $stubDir 'code.cmd') -Encoding ASCII -Value @(
|
|
'@echo off',
|
|
"echo %* >> `"$callLog`"",
|
|
'exit /b 0'
|
|
)
|
|
|
|
$server = $null
|
|
$savedPath = $env:PATH
|
|
try {
|
|
if (-not $SkipBuild) {
|
|
Write-Host 'building...' -ForegroundColor Cyan
|
|
& (Join-Path $scripts 'build.ps1') | Out-Null
|
|
}
|
|
$packages = @(Get-ChildItem -Path $dist -Filter '*.vsix')
|
|
if ($packages.Count -eq 0) { throw "no .vsix in $dist" }
|
|
|
|
Write-Host "starting the stub on $base" -ForegroundColor Cyan
|
|
# Set before spawning: the child inherits the environment as it is at that moment.
|
|
$env:PORT = "$Port"
|
|
$server = Start-Process -FilePath 'node' `
|
|
-ArgumentList (Join-Path $PSScriptRoot 'fake-gitea.js') `
|
|
-WindowStyle Hidden -PassThru
|
|
|
|
$up = $false
|
|
foreach ($attempt in 1..20) {
|
|
try { Get-State | Out-Null; $up = $true; break } catch { Start-Sleep -Milliseconds 250 }
|
|
}
|
|
if (-not $up) { throw "the stub did not come up on $base" }
|
|
|
|
$env:GITEA_URL = $base
|
|
$env:GITEA_OWNER = 'max'
|
|
$env:GITEA_REPO = 'vscode-extensions'
|
|
$env:GITEA_TOKEN = 'TESTTOKEN'
|
|
$env:PATH = "$stubDir;$savedPath"
|
|
|
|
Write-Host ''
|
|
Write-Host 'publish.ps1' -ForegroundColor Cyan
|
|
& (Join-Path $scripts 'publish.ps1') -Tag 'test' | Out-Null
|
|
$state = Get-State
|
|
$release = $state.releases | Where-Object { $_.tag_name -eq 'test' }
|
|
Assert-That 'the release is created' ($null -ne $release)
|
|
Assert-That "all $($packages.Count) packages are uploaded" `
|
|
($release.assets.Count -eq $packages.Count) "got $($release.assets.Count)"
|
|
|
|
$intact = $true
|
|
foreach ($asset in $release.assets) {
|
|
$local = Join-Path $dist $asset.name
|
|
if ((Get-FileHash $local -Algorithm SHA256).Hash -ne $asset.sha256) { $intact = $false }
|
|
}
|
|
Assert-That 'the uploaded bytes match dist/ exactly' $intact
|
|
|
|
$quoted = @($state.log | Where-Object { $_ -like '*disposition*' })
|
|
Assert-That 'the multipart field is name="attachment"' `
|
|
(($quoted.Count -gt 0) -and ($quoted[-1] -like '*name="attachment"*')) `
|
|
($(if ($quoted.Count) { $quoted[-1] } else { 'no disposition logged' }))
|
|
|
|
Write-Host ''
|
|
Write-Host 'publish.ps1 again' -ForegroundColor Cyan
|
|
& (Join-Path $scripts 'publish.ps1') -Tag 'test' | Out-Null
|
|
$release = (Get-State).releases | Where-Object { $_.tag_name -eq 'test' }
|
|
Assert-That 'republishing replaces rather than duplicates' `
|
|
($release.assets.Count -eq $packages.Count) "got $($release.assets.Count)"
|
|
|
|
Write-Host ''
|
|
Write-Host 'install.ps1' -ForegroundColor Cyan
|
|
Remove-Item $callLog -ErrorAction SilentlyContinue
|
|
& (Join-Path $scripts 'install.ps1') -Tag 'test' | Out-Null
|
|
$calls = @(Get-Content $callLog -ErrorAction SilentlyContinue)
|
|
Assert-That "code was called once per extension" ($calls.Count -eq $packages.Count) `
|
|
"got $($calls.Count)"
|
|
Assert-That 'every install passes --force' `
|
|
(($calls.Count -gt 0) -and -not ($calls | Where-Object { $_ -notmatch '--force' }))
|
|
|
|
$downloadedOk = $calls.Count -gt 0
|
|
foreach ($call in $calls) {
|
|
$path = ($call -replace '^--install-extension\s+', '' -replace '\s+--force\s*$', '').Trim()
|
|
$local = Join-Path $dist (Split-Path -Leaf $path)
|
|
if (-not (Test-Path $path) -or
|
|
(Get-FileHash $path -Algorithm SHA256).Hash -ne
|
|
(Get-FileHash $local -Algorithm SHA256).Hash) { $downloadedOk = $false }
|
|
}
|
|
Assert-That 'the downloaded files match dist/ (token survived the redirect)' $downloadedOk
|
|
|
|
Write-Host ''
|
|
Write-Host 'no Gitea configured' -ForegroundColor Cyan
|
|
$env:GITEA_URL = ''; $env:GITEA_OWNER = ''; $env:GITEA_REPO = ''
|
|
Remove-Item $callLog -ErrorAction SilentlyContinue
|
|
$localWorked = $true
|
|
try { & (Join-Path $scripts 'install.ps1') -Local | Out-Null } catch { $localWorked = $false }
|
|
Assert-That 'install.ps1 -Local needs no Gitea config' $localWorked
|
|
$env:GITEA_URL = $base; $env:GITEA_OWNER = 'max'; $env:GITEA_REPO = 'vscode-extensions'
|
|
|
|
# Git's bash, not the one on PATH: on Windows that is usually
|
|
# System32\bash.exe, the WSL launcher, which sees D:\ as /mnt/d and so cannot open
|
|
# the script by the path we have.
|
|
$bash = @(
|
|
'C:\Program Files\Git\bin\bash.exe',
|
|
'C:\Program Files (x86)\Git\bin\bash.exe',
|
|
(Join-Path $env:LOCALAPPDATA 'Programs\Git\bin\bash.exe')
|
|
) | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
|
|
if (-not $bash) {
|
|
$onPath = Get-Command bash -ErrorAction SilentlyContinue
|
|
if ($onPath -and $onPath.Source -notlike "$env:WINDIR*") { $bash = $onPath.Source }
|
|
}
|
|
|
|
if ($bash) {
|
|
Write-Host ''
|
|
Write-Host 'ci-release.sh' -ForegroundColor Cyan
|
|
$env:GITEA_SERVER = $base
|
|
$env:TAG = 'test-ci'
|
|
$env:SKIP_PACKAGE = '1'
|
|
# Forward slashes too: bash reads the backslashes in a Windows path as escapes,
|
|
# so D:\a\b arrives as D:ab.
|
|
$shPath = (Join-Path $scripts 'ci-release.sh') -replace '\\', '/'
|
|
& $bash $shPath 2>&1 | Out-Null
|
|
$ci = (Get-State).releases | Where-Object { $_.tag_name -eq 'test-ci' }
|
|
Assert-That 'ci-release.sh publishes the same set' `
|
|
(($null -ne $ci) -and ($ci.assets.Count -eq $packages.Count))
|
|
$ciIntact = $null -ne $ci
|
|
if ($ci) {
|
|
foreach ($asset in $ci.assets) {
|
|
$local = Join-Path $dist $asset.name
|
|
if ((Get-FileHash $local -Algorithm SHA256).Hash -ne $asset.sha256) {
|
|
$ciIntact = $false
|
|
}
|
|
}
|
|
}
|
|
Assert-That 'ci-release.sh uploads intact bytes' $ciIntact
|
|
Remove-Item Env:\SKIP_PACKAGE, Env:\TAG, Env:\GITEA_SERVER -ErrorAction SilentlyContinue
|
|
} else {
|
|
Write-Host ' SKIP ci-release.sh (no bash on PATH)' -ForegroundColor Yellow
|
|
}
|
|
} finally {
|
|
$env:PATH = $savedPath
|
|
Remove-Item Env:\GITEA_URL, Env:\GITEA_OWNER, Env:\GITEA_REPO, Env:\GITEA_TOKEN, Env:\PORT `
|
|
-ErrorAction SilentlyContinue
|
|
if ($server -and -not $server.HasExited) { Stop-Process -Id $server.Id -Force }
|
|
Remove-Item $stubDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Write-Host ''
|
|
if ($failures.Count -eq 0) {
|
|
Write-Host 'all checks passed' -ForegroundColor Green
|
|
exit 0
|
|
}
|
|
Write-Host "$($failures.Count) check(s) failed:" -ForegroundColor Red
|
|
$failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
|
|
exit 1
|