diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 675f3fe..efc1d3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -352,7 +352,8 @@ jobs: CHOCO_DIR: ${{ matrix.dir }} steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 - - name: Get checksums from release + - id: checksums + name: Get checksums from release shell: pwsh env: GH_TOKEN: ${{ github.token }} @@ -365,25 +366,22 @@ jobs: if (-not $arm64Hash) { Write-Error "no checksum for $env:ARM64_ASSET in checksums.txt"; exit 1 } echo "X64_HASH=$x64Hash" >> $env:GITHUB_ENV echo "ARM64_HASH=$arm64Hash" >> $env:GITHUB_ENV - - name: Update version and checksums - shell: pwsh - # paths are tool-local: working-directory-relative so a monorepo's - # tools//packaging/chocolatey resolves (flat repos pass "."). - working-directory: ${{ inputs.working-directory }} - run: | - $nuspec = "$env:CHOCO_DIR/$env:CHOCO_ID.nuspec" - $content = Get-Content $nuspec -Raw - # fail loud if the placeholder isn't present — a silent no-op would pack the wrong version - if ($content -notmatch '0\.0\.0') { Write-Error "nuspec $nuspec has no 0.0.0 placeholder"; exit 1 } - $content -replace '0.0.0', "$env:VERSION" | Set-Content $nuspec - $script = "$env:CHOCO_DIR/tools/chocolateyInstall.ps1" - $content = Get-Content $script -Raw - # fail loud if a placeholder is absent — a silent no-op would push literal placeholders - if ($content -notmatch 'CHECKSUM_AMD64_PLACEHOLDER') { Write-Error "$script has no CHECKSUM_AMD64_PLACEHOLDER"; exit 1 } - if ($content -notmatch 'CHECKSUM_ARM64_PLACEHOLDER') { Write-Error "$script has no CHECKSUM_ARM64_PLACEHOLDER"; exit 1 } - $content = $content -replace 'CHECKSUM_AMD64_PLACEHOLDER', $env:X64_HASH - $content = $content -replace 'CHECKSUM_ARM64_PLACEHOLDER', $env:ARM64_HASH - Set-Content $script $content + echo "x64-hash=$x64Hash" >> $env:GITHUB_OUTPUT + echo "arm64-hash=$arm64Hash" >> $env:GITHUB_OUTPUT + - id: render + name: Update version, checksums, and download URLs + uses: $/actions/chocolatey-render + with: + working-directory: ${{ inputs.working-directory }} + package-directory: ${{ matrix.dir }} + package-id: ${{ matrix.id }} + version: ${{ needs.goreleaser.outputs.version }} + repository: ${{ github.repository }} + final-tag: ${{ needs.goreleaser.outputs.final-tag }} + x64-asset: ${{ matrix.x64_asset }} + arm64-asset: ${{ matrix.arm64_asset }} + x64-hash: ${{ steps.checksums.outputs.x64-hash }} + arm64-hash: ${{ steps.checksums.outputs.arm64-hash }} - name: Pack and push uses: $/actions/chocolatey-push with: @@ -391,6 +389,7 @@ jobs: working-directory: ${{ inputs.working-directory }} package-directory: ${{ matrix.dir }} api-key: ${{ secrets.chocolatey-api-key }} + require-static-urls: ${{ steps.render.outputs.static-urls }} # winget/linux are best-effort: Microsoft-hosted flakiness must not gate the # binary/Homebrew artifacts. Failure surfaces in the step summary. diff --git a/.github/workflows/test-actions.yml b/.github/workflows/test-actions.yml index 045a954..1f2b20c 100644 --- a/.github/workflows/test-actions.yml +++ b/.github/workflows/test-actions.yml @@ -195,6 +195,15 @@ jobs: shell: bash working-directory: actions/chocolatey-push + # Chocolatey renderer: exact per-package asset URLs, migration opt-in, and + # legacy-template compatibility on the same PowerShell script used in release.yml. + chocolatey-render: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - run: ./actions/chocolatey-render/test_render.ps1 + shell: pwsh + # identity.py unit tests — PASS + every drift rule + missing-manifest, in tmp dirs. identity-unit: runs-on: ubuntu-latest diff --git a/actions/chocolatey-push/action.yml b/actions/chocolatey-push/action.yml index bf3848e..90c2ab0 100644 --- a/actions/chocolatey-push/action.yml +++ b/actions/chocolatey-push/action.yml @@ -15,6 +15,10 @@ inputs: api-key: description: Chocolatey API key. required: true + require-static-urls: + description: Reject packages whose install scripts retain URL placeholders or runtime version expressions. + required: false + default: "false" runs: using: composite steps: @@ -27,10 +31,16 @@ runs: PACKAGE_ID: ${{ inputs.package-id }} WORKING_DIRECTORY: ${{ inputs.working-directory }} PACKAGE_DIRECTORY: ${{ inputs.package-directory }} + REQUIRE_STATIC_URLS: ${{ inputs.require-static-urls }} run: | python -m pip install "defusedxml==0.7.1" + static_args=() + if [ "$REQUIRE_STATIC_URLS" = "true" ]; then + static_args+=(--require-static-urls) + fi python "$GITHUB_ACTION_PATH/chocolatey_push.py" push \ --package-id "$PACKAGE_ID" \ --working-directory "$WORKING_DIRECTORY" \ --package-directory "$PACKAGE_DIRECTORY" \ - --api-key-env CHOCO_API_KEY + --api-key-env CHOCO_API_KEY \ + "${static_args[@]}" diff --git a/actions/chocolatey-push/chocolatey_push.py b/actions/chocolatey-push/chocolatey_push.py index 4bd3fca..91442f2 100644 --- a/actions/chocolatey-push/chocolatey_push.py +++ b/actions/chocolatey-push/chocolatey_push.py @@ -4,8 +4,10 @@ import argparse import os +import re import subprocess import sys +import zipfile from dataclasses import dataclass from pathlib import Path from urllib.error import HTTPError, URLError @@ -18,6 +20,9 @@ CHOCO_SOURCE = "https://push.chocolatey.org/" COMMUNITY_API = "https://community.chocolatey.org/api/v2" ATOM_NS = "{http://www.w3.org/2005/Atom}" +URL_PLACEHOLDERS = ("URL_AMD64_PLACEHOLDER", "URL_ARM64_PLACEHOLDER") +CHECKSUM_PLACEHOLDERS = ("CHECKSUM_AMD64_PLACEHOLDER", "CHECKSUM_ARM64_PLACEHOLDER") +RUNTIME_VERSION_EXPRESSION = re.compile(r"(?i)ChocolateyPackageVersion|\$\{\s*version\s*\}") class PushError(Exception): @@ -64,6 +69,7 @@ def pack_and_push( command_runner=None, http_get=None, summary_path: str | None = None, + require_static_urls: bool = False, ) -> int: if not api_key: raise PushError("chocolatey-api-key secret is required") @@ -77,6 +83,8 @@ def pack_and_push( packages = sorted(choco_dir.glob("*.nupkg")) if len(packages) != 1: raise PushError(f"expected exactly one .nupkg, found {len(packages)}") + if require_static_urls: + validate_static_package(packages[0]) push = run_command( [ @@ -115,6 +123,28 @@ def pack_and_push( return 0 +def validate_static_package(package: Path) -> None: + """Reject migrated packages that still defer release URLs to install time.""" + try: + with zipfile.ZipFile(package) as archive: + scripts = [name for name in archive.namelist() if name.lower().endswith((".ps1", ".nuspec"))] + if not scripts: + raise PushError(f"{package.name} contains no Chocolatey scripts or nuspec") + if not any(name.replace("\\", "/").rsplit("/", 1)[-1].lower() == "chocolateyinstall.ps1" for name in scripts): + raise PushError(f"{package.name} does not contain tools/chocolateyInstall.ps1") + for name in scripts: + # Windows PowerShell 5.1 may pack UTF-16 scripts; removing its + # NUL padding keeps the token checks encoding-independent. + text = archive.read(name).decode("utf-8", errors="replace").replace("\x00", "") + leftovers = [token for token in URL_PLACEHOLDERS + CHECKSUM_PLACEHOLDERS if token in text] + if leftovers: + raise PushError(f"{package.name}/{name} retains placeholders: {', '.join(leftovers)}") + if RUNTIME_VERSION_EXPRESSION.search(text): + raise PushError(f"{package.name}/{name} retains a runtime package-version expression") + except zipfile.BadZipFile as exc: + raise PushError(f"cannot inspect invalid Chocolatey package {package.name}") from exc + + def run_command(command: list[str], cwd: Path, command_runner=None) -> CommandResult: command_runner = command_runner or subprocess.run result = command_runner(command, cwd=cwd, text=True, capture_output=True) @@ -211,6 +241,7 @@ def main(argv: list[str] | None = None) -> int: push.add_argument("--working-directory", default=".") push.add_argument("--package-directory", default="packaging/chocolatey") push.add_argument("--api-key-env", default="CHOCO_API_KEY") + push.add_argument("--require-static-urls", action="store_true") args = parser.parse_args(argv) try: @@ -220,6 +251,7 @@ def main(argv: list[str] | None = None) -> int: working_dir=args.working_directory, package_dir=args.package_directory, api_key=os.environ.get(args.api_key_env, ""), + require_static_urls=args.require_static_urls, ) except (PushError, ProbeError) as exc: print(f"::error::{exc}", file=sys.stderr) diff --git a/actions/chocolatey-push/test_chocolatey_push.py b/actions/chocolatey-push/test_chocolatey_push.py index bc46e27..2c5d722 100644 --- a/actions/chocolatey-push/test_chocolatey_push.py +++ b/actions/chocolatey-push/test_chocolatey_push.py @@ -1,3 +1,4 @@ +import zipfile from types import SimpleNamespace from urllib.error import URLError @@ -101,6 +102,64 @@ def runner(command, cwd, text, capture_output): ) +@pytest.mark.parametrize( + "script, encoding, error", + [ + ("$url = 'URL_AMD64_PLACEHOLDER'", "utf-8", "retains placeholders"), + ("$url = 'https://example.test/$env:ChocolateyPackageVersion'", "utf-16", "runtime package-version"), + ], +) +def test_static_package_validation_rejects_unrendered_install_script(tmp_path, script, encoding, error): + work = _working_dir(tmp_path) + calls = [] + + def runner(command, cwd, **_kwargs): + calls.append(command) + if command == ["choco", "pack"]: + with zipfile.ZipFile(cwd / "codereview-cli.1.0.0.nupkg", "w") as archive: + archive.writestr("tools/chocolateyInstall.ps1", script.encode(encoding)) + return _result(0) + raise AssertionError("static package validation must run before choco push") + + with pytest.raises(chocolatey_push.PushError, match=error): + chocolatey_push.pack_and_push( + package_id="codereview-cli", + working_dir=work, + api_key="key", + command_runner=runner, + require_static_urls=True, + ) + assert calls == [["choco", "pack"]] + + +def test_static_package_validation_accepts_rendered_package(tmp_path): + package = tmp_path / "package.nupkg" + with zipfile.ZipFile(package, "w") as archive: + archive.writestr( + "tools/chocolateyInstall.ps1", + "\n".join( + [ + "$url64 = 'https://github.com/open-cli-collective/example/releases/download/v1.2.3/example-amd64.zip'", + "$urlArm64 = 'https://github.com/open-cli-collective/example/releases/download/v1.2.3/example-arm64.zip'", + "$checksum64 = 'a' * 64", + "$checksumArm64 = 'b' * 64", + ] + ), + ) + archive.writestr("example.nuspec", "1.2.3") + + assert chocolatey_push.validate_static_package(package) is None + + +def test_static_package_validation_requires_install_script(tmp_path): + package = tmp_path / "package.nupkg" + with zipfile.ZipFile(package, "w") as archive: + archive.writestr("example.nuspec", "1.2.3") + + with pytest.raises(chocolatey_push.PushError, match="chocolateyInstall.ps1"): + chocolatey_push.validate_static_package(package) + + def test_non_forbidden_push_failure_fails_release(tmp_path): def runner(command, cwd, text, capture_output): if command == ["choco", "pack"]: diff --git a/actions/chocolatey-render/action.yml b/actions/chocolatey-render/action.yml new file mode 100644 index 0000000..bc8b7fd --- /dev/null +++ b/actions/chocolatey-render/action.yml @@ -0,0 +1,68 @@ +name: Render Chocolatey package +description: Inject the release version, checksums, and optional static asset URLs into a Chocolatey package. +inputs: + working-directory: + description: Repository-relative tool working directory. + required: false + default: "." + package-directory: + description: Chocolatey package directory relative to working-directory. + required: true + package-id: + description: Chocolatey package id. + required: true + version: + description: Release version for the nuspec. + required: true + repository: + description: GitHub repository containing the release assets, owner/name. + required: true + final-tag: + description: GitHub release tag containing the published assets. + required: true + x64-asset: + description: Published AMD64 Windows asset name. + required: true + arm64-asset: + description: Published ARM64 Windows asset name. + required: true + x64-hash: + description: SHA256 for the AMD64 Windows asset. + required: true + arm64-hash: + description: SHA256 for the ARM64 Windows asset. + required: true +outputs: + static-urls: + description: true when both URL placeholders were rendered into literal release URLs. + value: ${{ steps.render.outputs.static-urls }} +runs: + using: composite + steps: + - id: render + shell: pwsh + env: + ACTION_PATH: ${{ github.action_path }} + WORKING_DIRECTORY: ${{ inputs.working-directory }} + PACKAGE_DIRECTORY: ${{ inputs.package-directory }} + PACKAGE_ID: ${{ inputs.package-id }} + VERSION: ${{ inputs.version }} + GITHUB_REPOSITORY: ${{ inputs.repository }} + FINAL_TAG: ${{ inputs.final-tag }} + X64_ASSET: ${{ inputs.x64-asset }} + ARM64_ASSET: ${{ inputs.arm64-asset }} + X64_HASH: ${{ inputs.x64-hash }} + ARM64_HASH: ${{ inputs.arm64-hash }} + run: | + & "$env:ACTION_PATH/render.ps1" ` + -WorkingDirectory "$env:WORKING_DIRECTORY" ` + -PackageDirectory "$env:PACKAGE_DIRECTORY" ` + -PackageId "$env:PACKAGE_ID" ` + -Version "$env:VERSION" ` + -Repository "$env:GITHUB_REPOSITORY" ` + -FinalTag "$env:FINAL_TAG" ` + -X64Asset "$env:X64_ASSET" ` + -Arm64Asset "$env:ARM64_ASSET" ` + -X64Hash "$env:X64_HASH" ` + -Arm64Hash "$env:ARM64_HASH" ` + -GitHubOutput "$env:GITHUB_OUTPUT" diff --git a/actions/chocolatey-render/render.ps1 b/actions/chocolatey-render/render.ps1 new file mode 100644 index 0000000..5b987d3 --- /dev/null +++ b/actions/chocolatey-render/render.ps1 @@ -0,0 +1,81 @@ +[CmdletBinding()] +param( + [string]$WorkingDirectory = ".", + [string]$PackageDirectory, + [string]$PackageId, + [string]$Version, + [string]$Repository, + [string]$FinalTag, + [string]$X64Asset, + [string]$Arm64Asset, + [string]$X64Hash, + [string]$Arm64Hash, + [string]$GitHubOutput = "" +) + +$ErrorActionPreference = 'Stop' + +$requiredInputs = @{ + WorkingDirectory = $WorkingDirectory + PackageDirectory = $PackageDirectory + PackageId = $PackageId + Version = $Version + X64Hash = $X64Hash + Arm64Hash = $Arm64Hash +} +foreach ($required in $requiredInputs.GetEnumerator()) { + if ([string]::IsNullOrWhiteSpace($required.Value)) { + throw "$($required.Key) is required" + } +} + +$nuspec = Join-Path $WorkingDirectory (Join-Path $PackageDirectory "$PackageId.nuspec") +$script = Join-Path $WorkingDirectory (Join-Path $PackageDirectory "tools/chocolateyInstall.ps1") + +$content = Get-Content $nuspec -Raw +if ($content -notmatch '0\.0\.0') { + throw "nuspec $nuspec has no 0.0.0 placeholder" +} +$content -replace '0\.0\.0', "$Version" | Set-Content $nuspec + +$content = Get-Content $script -Raw +if ($content -notmatch 'CHECKSUM_AMD64_PLACEHOLDER') { + throw "$script has no CHECKSUM_AMD64_PLACEHOLDER" +} +if ($content -notmatch 'CHECKSUM_ARM64_PLACEHOLDER') { + throw "$script has no CHECKSUM_ARM64_PLACEHOLDER" +} + +# Both URL placeholders opt a package into static rendering. Legacy templates +# omit both and keep their existing runtime URL path during migration. +$hasAmd64Url = $content.Contains('URL_AMD64_PLACEHOLDER') +$hasArm64Url = $content.Contains('URL_ARM64_PLACEHOLDER') +if ($hasAmd64Url -xor $hasArm64Url) { + throw "$script must contain both URL_AMD64_PLACEHOLDER and URL_ARM64_PLACEHOLDER" +} +$staticUrls = $hasAmd64Url -and $hasArm64Url +if ($staticUrls) { + if ([string]::IsNullOrWhiteSpace($Repository) -or [string]::IsNullOrWhiteSpace($FinalTag) -or [string]::IsNullOrWhiteSpace($X64Asset) -or [string]::IsNullOrWhiteSpace($Arm64Asset)) { + throw "$script cannot render static URLs without repository, final tag, and both asset names" + } + if ($content -match '(?i)(?:ChocolateyPackageVersion|\$\{\s*version\s*\})') { + throw "$script retains a runtime package-version expression; migrated packages require literal release URLs" + } + $x64Url = "https://github.com/$Repository/releases/download/$FinalTag/$X64Asset" + $arm64Url = "https://github.com/$Repository/releases/download/$FinalTag/$Arm64Asset" + $content = $content.Replace('URL_AMD64_PLACEHOLDER', $x64Url) + $content = $content.Replace('URL_ARM64_PLACEHOLDER', $arm64Url) +} + +$content = $content -replace 'CHECKSUM_AMD64_PLACEHOLDER', $X64Hash +$content = $content -replace 'CHECKSUM_ARM64_PLACEHOLDER', $Arm64Hash +if ($staticUrls -and $content -match 'URL_(?:AMD64|ARM64)_PLACEHOLDER|CHECKSUM_(?:AMD64|ARM64)_PLACEHOLDER|(?i:ChocolateyPackageVersion|\$\{\s*version\s*\})') { + throw "$script still contains a URL/checksum placeholder or runtime package-version expression after rendering" +} +Set-Content $script $content + +$staticOutput = $staticUrls.ToString().ToLowerInvariant() +if (-not [string]::IsNullOrWhiteSpace($GitHubOutput)) { + Add-Content -Path $GitHubOutput -Value "static-urls=$staticOutput" +} +Write-Output "static-urls=$staticOutput" diff --git a/actions/chocolatey-render/test_render.ps1 b/actions/chocolatey-render/test_render.ps1 new file mode 100644 index 0000000..459e6f8 --- /dev/null +++ b/actions/chocolatey-render/test_render.ps1 @@ -0,0 +1,140 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +$renderer = Join-Path $PSScriptRoot 'render.ps1' +$root = Join-Path ([IO.Path]::GetTempPath()) "chocolatey-render-$([guid]::NewGuid())" +New-Item -ItemType Directory -Path $root | Out-Null + +function New-Fixture { + param( + [string]$Name, + [string]$InstallScript + ) + $package = Join-Path $root "$Name/packaging/chocolatey" + New-Item -ItemType Directory -Path (Join-Path $package 'tools') -Force | Out-Null + Set-Content (Join-Path $package "$Name.nuspec") '0.0.0' + Set-Content (Join-Path $package 'tools/chocolateyInstall.ps1') $InstallScript + [pscustomobject]@{ + Name = $Name + WorkingDirectory = Join-Path $root $Name + PackageDirectory = 'packaging/chocolatey' + Script = Join-Path $package 'tools/chocolateyInstall.ps1' + Nuspec = Join-Path $package "$Name.nuspec" + Output = Join-Path $root "$Name-output.txt" + } +} + +function Assert-Contains { + param([string]$Text, [string]$Expected, [string]$Message) + if (-not $Text.Contains($Expected)) { + throw "${Message}: expected '$Expected'" + } +} + +function Assert-NotContains { + param([string]$Text, [string]$Unexpected, [string]$Message) + if ($Text.Contains($Unexpected)) { + throw "${Message}: found '$Unexpected'" + } +} + +function Invoke-Render { + param( + [pscustomobject]$Fixture, + [string]$Repository = 'open-cli-collective/google-cli', + [string]$FinalTag = 'v2.0.123', + [string]$X64Asset = 'google_windows_amd64.zip', + [string]$Arm64Asset = 'google_windows_arm64.zip', + [string]$X64Hash = ('a' * 64), + [string]$Arm64Hash = ('b' * 64) + ) + & $renderer ` + -WorkingDirectory $Fixture.WorkingDirectory ` + -PackageDirectory $Fixture.PackageDirectory ` + -PackageId $Fixture.Name ` + -Version '2.0.123' ` + -Repository $Repository ` + -FinalTag $FinalTag ` + -X64Asset $X64Asset ` + -Arm64Asset $Arm64Asset ` + -X64Hash $X64Hash ` + -Arm64Hash $Arm64Hash ` + -GitHubOutput $Fixture.Output +} + +function Assert-RenderFails { + param( + [scriptblock]$Action, + [string]$ExpectedMessage + ) + $matched = $false + try { + & $Action + } catch { + $matched = $_.Exception.Message -match $ExpectedMessage + } + if (-not $matched) { + throw "renderer should fail with '$ExpectedMessage'" + } +} + +try { + $packages = @( + @{ Name = 'google-readonly'; X64 = 'gro_v2.0.123_windows_amd64.zip'; ARM64 = 'gro_v2.0.123_windows_arm64.zip'; X64Hash = ('1' * 64); ARM64Hash = ('2' * 64) }, + @{ Name = 'google-readwrite'; X64 = 'grw_v2.0.123_windows_amd64.zip'; ARM64 = 'grw_v2.0.123_windows_arm64.zip'; X64Hash = ('3' * 64); ARM64Hash = ('4' * 64) } + ) + foreach ($package in $packages) { + $fixture = New-Fixture $package.Name @" +`$url64 = 'URL_AMD64_PLACEHOLDER' +`$urlArm64 = 'URL_ARM64_PLACEHOLDER' +`$checksum64 = 'CHECKSUM_AMD64_PLACEHOLDER' +`$checksumArm64 = 'CHECKSUM_ARM64_PLACEHOLDER' +"@ + Invoke-Render $fixture -X64Asset $package.X64 -Arm64Asset $package.ARM64 -X64Hash $package.X64Hash -Arm64Hash $package.ARM64Hash + $rendered = Get-Content $fixture.Script -Raw + $expectedBase = "https://github.com/open-cli-collective/google-cli/releases/download/v2.0.123/" + Assert-Contains $rendered "$expectedBase$($package.X64)" "$($package.Name) AMD64 URL" + Assert-Contains $rendered "$expectedBase$($package.ARM64)" "$($package.Name) ARM64 URL" + Assert-Contains $rendered $package.X64Hash "$($package.Name) AMD64 hash" + Assert-Contains $rendered $package.ARM64Hash "$($package.Name) ARM64 hash" + Assert-NotContains $rendered 'URL_AMD64_PLACEHOLDER' "$($package.Name) AMD64 placeholder" + Assert-NotContains $rendered 'URL_ARM64_PLACEHOLDER' "$($package.Name) ARM64 placeholder" + Assert-Contains (Get-Content $fixture.Output -Raw) 'static-urls=true' "$($package.Name) static output" + Assert-Contains (Get-Content $fixture.Nuspec -Raw) '2.0.123' "$($package.Name) version" + } + + $mixed = New-Fixture 'mixed' @" +`$url64 = 'URL_AMD64_PLACEHOLDER' +`$urlArm64 = 'https://example.test/arm64.zip' +`$checksum64 = 'CHECKSUM_AMD64_PLACEHOLDER' +`$checksumArm64 = 'CHECKSUM_ARM64_PLACEHOLDER' +"@ + Assert-RenderFails { Invoke-Render $mixed } 'both URL_AMD64_PLACEHOLDER and URL_ARM64_PLACEHOLDER' + + $missing = New-Fixture 'missing-input' @" +`$url64 = 'URL_AMD64_PLACEHOLDER' +`$urlArm64 = 'URL_ARM64_PLACEHOLDER' +`$checksum64 = 'CHECKSUM_AMD64_PLACEHOLDER' +`$checksumArm64 = 'CHECKSUM_ARM64_PLACEHOLDER' +"@ + Assert-RenderFails { Invoke-Render $missing -Repository '' } 'repository, final tag, and both asset names' + + $legacy = New-Fixture 'legacy' @" +`$version = `$env:ChocolateyPackageVersion +`$url64 = "https://example.test/v`${version}/amd64.zip" +`$urlArm64 = "https://example.test/v`${version}/arm64.zip" +`$checksum64 = 'CHECKSUM_AMD64_PLACEHOLDER' +`$checksumArm64 = 'CHECKSUM_ARM64_PLACEHOLDER' +"@ + Invoke-Render $legacy -Repository '' -FinalTag '' -X64Asset '' -Arm64Asset '' + $legacyRendered = Get-Content $legacy.Script -Raw + Assert-Contains $legacyRendered 'ChocolateyPackageVersion' 'legacy runtime URL' + Assert-Contains $legacyRendered ('a' * 64) 'legacy AMD64 hash' + Assert-Contains $legacyRendered ('b' * 64) 'legacy ARM64 hash' + Assert-Contains (Get-Content $legacy.Output -Raw) 'static-urls=false' 'legacy static output' + + Write-Output 'chocolatey renderer checks passed' +} finally { + Remove-Item $root -Recurse -Force -ErrorAction SilentlyContinue +}