From bf98af4b83e2887d46209519595d0cfec344d0e9 Mon Sep 17 00:00:00 2001 From: neng Date: Mon, 21 Sep 2026 11:00:33 +0800 Subject: [PATCH] fix(release): synchronize 1.2.3 versions and reject mismatched tags --- .github/scripts/test-release-version.ps1 | 87 +++++++++++++++++++ .github/scripts/verify-release-version.ps1 | 49 +++++++++++ .github/workflows/ci.yml | 8 +- .github/workflows/release.yml | 33 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 24 ++++- .../lexmount-browser/references/commands.md | 2 +- skills/lexmount-browser/scripts/bootstrap.ps1 | 2 +- skills/lexmount-browser/scripts/bootstrap.sh | 2 +- 10 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 .github/scripts/test-release-version.ps1 create mode 100644 .github/scripts/verify-release-version.ps1 diff --git a/.github/scripts/test-release-version.ps1 b/.github/scripts/test-release-version.ps1 new file mode 100644 index 0000000..2e48c5c --- /dev/null +++ b/.github/scripts/test-release-version.ps1 @@ -0,0 +1,87 @@ +# Dependency-free release gate regression tests; compatible with PowerShell 5.1. +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +$validator = Join-Path $PSScriptRoot 'verify-release-version.ps1' +$fixture = Join-Path ([IO.Path]::GetTempPath()) ('browser-cli-version-' + [Guid]::NewGuid().ToString('N')) +$resolvedFixture = [IO.Path]::GetFullPath($fixture) +$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar +if (-not $resolvedFixture.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'Fixture is outside temporary directory' } +New-Item -ItemType Directory -Path (Join-Path $fixture 'skills/lexmount-browser/scripts') -Force | Out-Null +$cases = [Collections.Generic.List[string]]::new() +$utf8 = [Text.UTF8Encoding]::new($false) + +function Write-Fixture([string]$Path, [string]$Text) { + [IO.File]::WriteAllText((Join-Path $fixture $Path), $Text, $utf8) +} +function Reset-Fixture([string]$Version = '1.2.3') { + Write-Fixture 'Cargo.toml' "[package]`nname = `"lexmount-browser`"`nversion = `"$Version`"`n[dependencies]`nother = `"9.8.7`"`n" + Write-Fixture 'Cargo.lock' "version = 4`n[[package]]`nname = `"dependency`"`nversion = `"9.8.7`"`n[[package]]`nname = `"lexmount-browser`"`nversion = `"$Version`"`n" + Write-Fixture 'skills/lexmount-browser/scripts/bootstrap.ps1' ('$version = if ($env:LEXMOUNT_BROWSER_CLI_VERSION) { $env:LEXMOUNT_BROWSER_CLI_VERSION } else { "' + $Version + '" }' + "`n") + Write-Fixture 'skills/lexmount-browser/scripts/bootstrap.sh' ('version="${LEXMOUNT_BROWSER_CLI_VERSION:-' + $Version + '}"' + "`n") +} +function Pass([string]$Name, [scriptblock]$Action) { + & $Action + $cases.Add($Name) + Write-Host "PASS $Name" +} +function Reject([string]$Name, [scriptblock]$Action, [string]$Expected) { + $errorText = $null + try { & $Action | Out-Null } catch { $errorText = $_.Exception.Message } + if (-not $errorText -or $errorText -notlike "*$Expected*") { throw "$Name did not reject with '$Expected': $errorText" } + $cases.Add($Name) + Write-Host "PASS $Name" +} +try { + Reset-Fixture + Pass 'matching source and tag' { + $result = & $validator -RepositoryRoot $fixture -ReleaseTag 'v1.2.3' + if ($result.Version -cne '1.2.3' -or $result.VerifiedFiles -ne 4) { throw 'Unexpected validation result' } + } + Pass 'source-only PR check' { & $validator -RepositoryRoot $fixture | Out-Null } + foreach ($tag in @('v1.2.2', '1.2.3', 'V1.2.3', 'v1.2.3-extra', '')) { + Reject "wrong tag [$tag]" { & $validator -RepositoryRoot $fixture -ReleaseTag $tag } 'Release tag' + } + Reset-Fixture '1.2.1' + Reject 'published 1.2.2 incident: every source version still 1.2.1' { & $validator -RepositoryRoot $fixture -ReleaseTag 'v1.2.2' } 'Release tag' + Reset-Fixture + Write-Fixture 'Cargo.lock' "[[package]]`nname = `"lexmount-browser`"`nversion = `"1.2.1`"`n" + Reject 'stale lockfile' { & $validator -RepositoryRoot $fixture } 'Cargo.lock version' + Reset-Fixture + Write-Fixture 'skills/lexmount-browser/scripts/bootstrap.ps1' '$version = if ($env:LEXMOUNT_BROWSER_CLI_VERSION) { $env:LEXMOUNT_BROWSER_CLI_VERSION } else { "1.2.1" }' + Reject 'stale Windows bootstrap' { & $validator -RepositoryRoot $fixture } 'bootstrap.ps1 version' + Reset-Fixture + Write-Fixture 'skills/lexmount-browser/scripts/bootstrap.sh' 'version="${LEXMOUNT_BROWSER_CLI_VERSION:-1.2.1}"' + Reject 'stale Mac bootstrap' { & $validator -RepositoryRoot $fixture } 'bootstrap.sh version' + Reset-Fixture + Write-Fixture 'Cargo.lock' "[[package]]`nname = `"some-dependency`"`nversion = `"1.2.3`"`n" + Reject 'dependency version cannot stand in for package' { & $validator -RepositoryRoot $fixture } 'exactly one lexmount-browser' + Reset-Fixture + $lock = [IO.File]::ReadAllText((Join-Path $fixture 'Cargo.lock')) + Write-Fixture 'Cargo.lock' ($lock + "[[package]]`nname = `"lexmount-browser`"`nversion = `"1.2.3`"`n") + Reject 'duplicate package' { & $validator -RepositoryRoot $fixture } 'exactly one lexmount-browser' + Reset-Fixture + Write-Fixture 'skills/lexmount-browser/scripts/bootstrap.sh' '# version="${LEXMOUNT_BROWSER_CLI_VERSION:-1.2.3}"' + Reject 'comment is not a bootstrap version' { & $validator -RepositoryRoot $fixture } 'shell bootstrap default' + Reset-Fixture + $bootstrap = [IO.File]::ReadAllText((Join-Path $fixture 'skills/lexmount-browser/scripts/bootstrap.sh')) + Write-Fixture 'skills/lexmount-browser/scripts/bootstrap.sh' ($bootstrap + $bootstrap) + Reject 'duplicate bootstrap declarations' { & $validator -RepositoryRoot $fixture } 'shell bootstrap default' + Reset-Fixture '1.2.3-rc.1' + Pass 'matching prerelease' { & $validator -RepositoryRoot $fixture -ReleaseTag 'v1.2.3-rc.1' | Out-Null } + Reset-Fixture + foreach ($path in @('Cargo.toml', 'Cargo.lock', 'skills/lexmount-browser/scripts/bootstrap.ps1', 'skills/lexmount-browser/scripts/bootstrap.sh')) { + Write-Fixture $path ([IO.File]::ReadAllText((Join-Path $fixture $path)).Replace("`n", "`r`n")) + } + Pass 'Windows line endings' { & $validator -RepositoryRoot $fixture -ReleaseTag 'v1.2.3' | Out-Null } + Pass 'actual repository source' { & $validator | Out-Null } + Pass 'standalone -File entry point' { + $currentVersion = (& $validator).Version + $engine = (Get-Process -Id $PID).Path + & $engine -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $validator -ReleaseTag "v$currentVersion" | Out-Null + if ($LASTEXITCODE -ne 0) { throw "Standalone release gate exited $LASTEXITCODE" } + } + [pscustomobject]@{ Passed = $cases.Count; Cases = @($cases) } | ConvertTo-Json -Depth 3 +} finally { + # Only this invocation's prevalidated GUID fixture, never the temp root. + Remove-Item -LiteralPath $resolvedFixture -Recurse -Force +} diff --git a/.github/scripts/verify-release-version.ps1 b/.github/scripts/verify-release-version.ps1 new file mode 100644 index 0000000..f65723e --- /dev/null +++ b/.github/scripts/verify-release-version.ps1 @@ -0,0 +1,49 @@ +# Source-only release gate. Never runs bootstrap scripts or changes versions. +[CmdletBinding()] +param( + [string]$RepositoryRoot, + [AllowEmptyString()][string]$ReleaseTag +) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +if (-not $PSBoundParameters.ContainsKey('RepositoryRoot')) { + $RepositoryRoot = Join-Path $PSScriptRoot '../..' +} + +function Read-OneMatch([string]$Text, [string]$Pattern, [string]$Label) { + $found = [regex]::Matches($Text, $Pattern) + if ($found.Count -ne 1) { throw "Expected exactly one $Label; found $($found.Count)" } + return $found[0].Groups[1].Value +} + +# These deliberately accept the repository's literal version declarations only. +# Fail closed if the manifest/bootstrap layout changes; never guess a version +# from a comment, dependency, environment override or a different package. +$manifest = Get-Content -LiteralPath (Join-Path $RepositoryRoot 'Cargo.toml') -Raw +$package = Read-OneMatch $manifest '(?ms)^\[package\][ \t]*\r?\n(.*?)(?=^\[|\z)' 'Cargo.toml [package] section' +$name = Read-OneMatch $package '(?m)^name[ \t]*=[ \t]*"([^"]+)"[ \t]*\r?$' 'package name' +if ($name -cne 'lexmount-browser') { throw "Unexpected package name: $name" } +$version = Read-OneMatch $package '(?m)^version[ \t]*=[ \t]*"([^"]+)"[ \t]*\r?$' 'package version' +$semver = '(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?' +if ($version -cnotmatch "^$semver$") { throw "Invalid literal package version: $version" } + +$lock = Get-Content -LiteralPath (Join-Path $RepositoryRoot 'Cargo.lock') -Raw +$entries = @([regex]::Matches($lock, '(?ms)^\[\[package\]\][ \t]*\r?\n(.*?)(?=^\[|\z)') | + Where-Object { $_.Groups[1].Value -match '(?m)^name[ \t]*=[ \t]*"lexmount-browser"[ \t]*\r?$' }) +if ($entries.Count -ne 1) { throw 'Expected exactly one lexmount-browser entry in Cargo.lock' } +$lockVersion = Read-OneMatch $entries[0].Groups[1].Value '(?m)^version[ \t]*=[ \t]*"([^"]+)"[ \t]*\r?$' 'Cargo.lock package version' +$psBootstrap = Get-Content -LiteralPath (Join-Path $RepositoryRoot 'skills/lexmount-browser/scripts/bootstrap.ps1') -Raw +$shBootstrap = Get-Content -LiteralPath (Join-Path $RepositoryRoot 'skills/lexmount-browser/scripts/bootstrap.sh') -Raw +$psVersion = Read-OneMatch $psBootstrap '(?m)^\$version = if \(\$env:LEXMOUNT_BROWSER_CLI_VERSION\) \{ \$env:LEXMOUNT_BROWSER_CLI_VERSION \} else \{ "([^"]+)" \}[ \t]*\r?$' 'PowerShell bootstrap default' +$shVersion = Read-OneMatch $shBootstrap '(?m)^version="\$\{LEXMOUNT_BROWSER_CLI_VERSION:-([^}]+)\}"[ \t]*\r?$' 'shell bootstrap default' +foreach ($entry in @( + @{ Label = 'Cargo.lock'; Value = $lockVersion }, + @{ Label = 'bootstrap.ps1'; Value = $psVersion }, + @{ Label = 'bootstrap.sh'; Value = $shVersion } +)) { + if ($entry.Value -cne $version) { throw "$($entry.Label) version $($entry.Value) does not match Cargo.toml $version" } +} +if ($PSBoundParameters.ContainsKey('ReleaseTag') -and $ReleaseTag -cne "v$version") { + throw "Release tag '$ReleaseTag' does not match package/bootstrap version v$version" +} +[pscustomobject]@{ Version = $version; ReleaseTag = $ReleaseTag; VerifiedFiles = 4 } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a14a107..94b1379 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,11 +38,6 @@ jobs: ! grep -q 'skills/lexmount-browser/bin/' .github/workflows/release.yml - run: bash -n scripts/sign_and_notarize_macos.sh scripts/upload-release-to-cos.sh - run: sh -n scripts/package-skill.sh skills/lexmount-browser/scripts/bootstrap.sh skills/lexmount-browser/scripts/doctor.sh - - name: Verify Skill bootstrap version - run: | - package_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n 1)" - grep -Fq "else { \"${package_version}\" }" skills/lexmount-browser/scripts/bootstrap.ps1 - grep -Fq "LEXMOUNT_BROWSER_CLI_VERSION:-${package_version}" skills/lexmount-browser/scripts/bootstrap.sh - name: Verify Skill platform selection and PATH isolation run: | test_dir="$(mktemp -d)" @@ -81,6 +76,9 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 + - name: Test release version consistency gate + shell: powershell + run: .\.github\scripts\test-release-version.ps1 - name: Test published bootstrap version selection shell: powershell run: .\.github\scripts\test-select-bootstrap-version.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e4444c..9ac7d2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,20 @@ permissions: contents: write jobs: + validate-version: + runs-on: windows-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + - name: Validate tag, Cargo and Skill versions before releasing + shell: powershell + env: + RELEASE_TAG: ${{ github.ref_name }} + run: .\.github\scripts\verify-release-version.ps1 -ReleaseTag $env:RELEASE_TAG + build-macos: + needs: validate-version runs-on: macos-14 environment: macos-release steps: @@ -18,6 +31,12 @@ jobs: targets: aarch64-apple-darwin - run: cargo test --locked - run: cargo build --release --locked --target aarch64-apple-darwin + - name: Verify compiled version before signing + shell: bash + run: | + actual="$(target/aarch64-apple-darwin/release/browser-cli --version)" + expected="browser-cli ${GITHUB_REF_NAME#v}" + [ "$actual" = "$expected" ] || { echo "Version mismatch: expected $expected, got $actual" >&2; exit 1; } - name: Sign and notarize env: MACOS_DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_APPLICATION_P12_BASE64 }} @@ -38,6 +57,7 @@ jobs: path: browser-cli-v*-aarch64-apple-darwin* build-linux: + needs: validate-version runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 @@ -48,6 +68,12 @@ jobs: - run: cargo test --locked - name: Build static Linux binary run: cargo build --release --locked --target x86_64-unknown-linux-musl + - name: Verify compiled version before packaging + shell: bash + run: | + actual="$(target/x86_64-unknown-linux-musl/release/browser-cli --version)" + expected="browser-cli ${GITHUB_REF_NAME#v}" + [ "$actual" = "$expected" ] || { echo "Version mismatch: expected $expected, got $actual" >&2; exit 1; } - name: Package run: | version="${GITHUB_REF_NAME#v}" @@ -66,6 +92,7 @@ jobs: path: browser-cli-v*-x86_64-unknown-linux-musl* build-windows: + needs: validate-version runs-on: windows-latest steps: - uses: actions/checkout@v5 @@ -74,6 +101,12 @@ jobs: targets: x86_64-pc-windows-msvc - run: cargo test --locked - run: cargo build --release --locked --target x86_64-pc-windows-msvc + - name: Verify compiled version before packaging + shell: bash + run: | + actual="$(target/x86_64-pc-windows-msvc/release/browser-cli.exe --version)" + expected="browser-cli ${GITHUB_REF_NAME#v}" + [ "$actual" = "$expected" ] || { echo "Version mismatch: expected $expected, got $actual" >&2; exit 1; } - name: Package shell: bash run: | diff --git a/Cargo.lock b/Cargo.lock index c6e2e79..712fb37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1208,7 +1208,7 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "lexmount-browser" -version = "1.2.1" +version = "1.2.3" dependencies = [ "base64 0.22.1", "clap", diff --git a/Cargo.toml b/Cargo.toml index 2538fa8..267bde6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lexmount-browser" -version = "1.2.1" +version = "1.2.3" edition = "2024" license = "MIT" description = "Native Rust SDK and CLI for Lexmount cloud browsers" diff --git a/README.md b/README.md index 67881a3..c790bbb 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ automatic action retries. These changes require a new CLI release; published Explicit page selection is introduced in version 1.2.0. Check that the installed binary's `browser-cli action --help` lists `--target-id`; the published 1.1.15 binary does not have it. The package version and both bootstrap scripts target -1.2.1 together. Merging or building this source does not publish release assets: -bootstrap can install 1.2.1 only after its binaries and checksums are published +1.2.3 together. Merging or building this source does not publish release assets: +bootstrap can install 1.2.3 only after its binaries and checksums are published to COS. Until then, use a source build for local verification. Every `action` command accepts an optional `--target-id`. Obtain the page's CDP @@ -163,6 +163,26 @@ dependency rather than substituting an older binary for a task needing the new feature. Release tags must match the Cargo and bootstrap versions; never overwrite an existing release with changed binaries. +### Release checklist + +1. In a reviewed PR, update the package version in `Cargo.toml`, the + `lexmount-browser` entry in `Cargo.lock`, and the defaults in both + `skills/lexmount-browser/scripts/bootstrap.ps1` and `bootstrap.sh`. +2. Run `.github/scripts/test-release-version.ps1` with Windows PowerShell 5.1 + or PowerShell 7, then `.github/scripts/verify-release-version.ps1 -ReleaseTag + v1.2.3` (substitute the intended version). Complete CI and merge the PR. +3. Create the matching tag **on that merged commit**. Typing a new tag or + release title in GitHub does not update any source version. The release + workflow rejects inconsistent versions before building, signing or uploading. +4. Wait for every build and the publish job. Each platform's compiled binary + must report the tag's version before packaging. Verify the downloaded asset's + checksum and `browser-cli version`; the Skill ZIP must pin the same version. + +The published `v1.2.2` assets include the error-reporting fixes, but were built +with Cargo version `1.2.1` and Skill bootstrap defaults `1.2.1`. They are +misversioned; use the corrected `v1.2.3` release once published. Do not retag or +overwrite `v1.2.2`: consumers may already have its original files and checksums. + Agents resolve bundled scripts and binaries from the directory containing the loaded `SKILL.md`: Codex uses the absolute source path supplied in the Skill metadata, Claude Code uses `${CLAUDE_SKILL_DIR}`, and WorkBuddy/CodeBuddy uses diff --git a/skills/lexmount-browser/references/commands.md b/skills/lexmount-browser/references/commands.md index 155eccd..eb03d50 100644 --- a/skills/lexmount-browser/references/commands.md +++ b/skills/lexmount-browser/references/commands.md @@ -49,7 +49,7 @@ Introduced in 1.2.0; requires a binary whose `browser-cli action --help` lists Check the actual Skill-local binary, not just the version of these instructions. For an authorized upgrade, rerun the matching Skill-local bootstrap script only -after its pinned 1.2.0 release assets are available, then verify `version` and +after its pinned release assets are available, then verify `version` and `action --help`. A merged PR or a newer Skill file does not publish or replace the binary. If the release is unavailable or the upgrade is not authorized, report the dependency or capability limitation; do not send unsupported flags diff --git a/skills/lexmount-browser/scripts/bootstrap.ps1 b/skills/lexmount-browser/scripts/bootstrap.ps1 index 315e27b..559b88c 100644 --- a/skills/lexmount-browser/scripts/bootstrap.ps1 +++ b/skills/lexmount-browser/scripts/bootstrap.ps1 @@ -12,7 +12,7 @@ function Invoke-Tls12Download { } } -$version = if ($env:LEXMOUNT_BROWSER_CLI_VERSION) { $env:LEXMOUNT_BROWSER_CLI_VERSION } else { "1.2.1" } +$version = if ($env:LEXMOUNT_BROWSER_CLI_VERSION) { $env:LEXMOUNT_BROWSER_CLI_VERSION } else { "1.2.3" } $downloadBaseUrl = if ($env:LEXMOUNT_BROWSER_CLI_DOWNLOAD_BASE_URL) { $env:LEXMOUNT_BROWSER_CLI_DOWNLOAD_BASE_URL.TrimEnd('/') } else { "https://cli-bin-1377899528.cos.ap-nanjing.myqcloud.com/releases/browser-cli" } $architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } if ($architecture -ne "AMD64") { throw "Only Windows x64 is supported" } diff --git a/skills/lexmount-browser/scripts/bootstrap.sh b/skills/lexmount-browser/scripts/bootstrap.sh index fa15f4b..4bc64b8 100755 --- a/skills/lexmount-browser/scripts/bootstrap.sh +++ b/skills/lexmount-browser/scripts/bootstrap.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -version="${LEXMOUNT_BROWSER_CLI_VERSION:-1.2.1}" +version="${LEXMOUNT_BROWSER_CLI_VERSION:-1.2.3}" download_base_url="${LEXMOUNT_BROWSER_CLI_DOWNLOAD_BASE_URL:-https://cli-bin-1377899528.cos.ap-nanjing.myqcloud.com/releases/browser-cli}" repo="${download_base_url%/}/v${version}" case "$(uname -s)-$(uname -m)" in