From 03c57963592a9d9a5e909dc9de8b782696aa448e Mon Sep 17 00:00:00 2001 From: liyuanyang Date: Sun, 6 Sep 2026 18:39:33 +0800 Subject: [PATCH 1/4] fix(release): pin compare-link start to CHANGELOG's second versioned heading The compare-link check used a `*` glob between `/compare/` and `...v`, so any start version passed validation. Derive the expected start from the CHANGELOG's second versioned heading and compare the full link as an exact string; fail closed when the heading is missing. Also fix the releases/tag branch, whose `[${version}]` was a glob character class rather than a literal match. Bidirectional: 4 previously-passing tampered links now fail; correct compare and releases/tag links still pass. --- scripts/validate_release_version.sh | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/scripts/validate_release_version.sh b/scripts/validate_release_version.sh index 987a304..937ac84 100755 --- a/scripts/validate_release_version.sh +++ b/scripts/validate_release_version.sh @@ -93,11 +93,29 @@ while IFS= read -r version_link; do done < <(grep -F -- "[${version}]: " "${changelog}" || true) [[ "${#version_links[@]}" == 1 ]] || die "CHANGELOG.md: expected exactly one [${version}] comparison link" -if [[ "${version_links[0]}" != \ - "[${version}]: https://github.com/bytefolk/mem/releases/tag/v${version}" && - "${version_links[0]}" != \ - "[${version}]: https://github.com/bytefolk/mem/compare/"*"...v${version}" ]]; then - die "CHANGELOG.md: [${version}] link must terminate at v${version}" +compare_base="$(awk ' + /^## \[/ && $0 != "## [Unreleased]" { + if (seen++) { + gsub(/^## \[/, "", $0) + gsub(/\].*/, "", $0) + print $0 + exit + } + } +' "${changelog}")" + +expected_compare_link= +if [[ -n "${compare_base}" ]]; then + expected_compare_link="[${version}]: https://github.com/bytefolk/mem/compare/v${compare_base}...v${version}" +fi +expected_release_link="[${version}]: https://github.com/bytefolk/mem/releases/tag/v${version}" + +if [[ "${version_links[0]}" != "${expected_release_link}" ]] && + [[ -z "${expected_compare_link}" || "${version_links[0]}" != "${expected_compare_link}" ]]; then + if [[ -z "${compare_base}" ]]; then + die "CHANGELOG.md: cannot derive compare base (need a second versioned heading below [${version}])" + fi + die "CHANGELOG.md: [${version}] link must be exactly:"$'\n'" ${expected_release_link}"$'\n'" or:"$'\n'" ${expected_compare_link}" fi printf 'PASS: all release version surfaces match %s\n' "${version}" From 1624cf7484f34e6b10c216ec31eda0652e2cd9b4 Mon Sep 17 00:00:00 2001 From: liyuanyang Date: Thu, 10 Sep 2026 09:47:59 +0800 Subject: [PATCH 2/4] fix(release): portable find and safe empty-array guard for macOS Replace GNU find -printf with -exec basename for macOS compatibility. Guard actual_assets expansion under set -u to prevent unbound variable error when the asset directory is empty. --- scripts/generate_release_checksums.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/generate_release_checksums.sh b/scripts/generate_release_checksums.sh index 4fb27fd..a99d96c 100755 --- a/scripts/generate_release_checksums.sh +++ b/scripts/generate_release_checksums.sh @@ -30,9 +30,9 @@ actual_assets=() while IFS= read -r actual_asset; do actual_assets[${#actual_assets[@]}]="${actual_asset}" done < <( - find "${asset_dir}" -mindepth 1 -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort + find "${asset_dir}" -mindepth 1 -maxdepth 1 -type f -exec basename {} + | LC_ALL=C sort ) -if [[ "${actual_assets[*]}" != "${assets[*]}" ]]; then +if [[ "${#actual_assets[@]}" -eq 0 ]] || [[ "${actual_assets[*]}" != "${assets[*]}" ]]; then printf 'ERROR: release assets differ from the exact expected set\n' >&2 printf 'expected: %s\n' "${assets[*]}" >&2 printf 'actual: %s\n' "${actual_assets[*]:-}" >&2 From 59c8b1cdf030d2169591b6e72faf86fa867d2325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:23:15 +0800 Subject: [PATCH 3/4] chore(web): refresh audited development dependencies (cherry picked from commit 11e02e21ef2c3dbd2dae26e4376872e54e78ecb5) --- web/package-lock.json | 100 +++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 103ae42..28a95f0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2161,16 +2161,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2179,13 +2179,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2206,9 +2206,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -2219,13 +2219,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -2233,14 +2233,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2249,9 +2249,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -2259,13 +2259,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3784,9 +3784,9 @@ "peer": true }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -4847,9 +4847,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6009,19 +6009,19 @@ } }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -6049,12 +6049,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" From fa2c30cc693bc0ae1e679e7d108d59aef1bd4007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Thu, 10 Sep 2026 12:23:13 +0800 Subject: [PATCH 4/4] fix(release): validate asset enumeration with GNU tools --- CHANGELOG.md | 3 ++ scripts/generate_release_checksums.sh | 2 +- scripts/test_release_guards.sh | 54 ++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 104b02f..db25a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,9 @@ The project publishes 0.x prerelease versions; a stable release line is not yet ### Fixed +- Release validation requires comparison links to start at the preceding + CHANGELOG release. Checksum generation handles each asset path separately on + GNU and BSD tools, including directories with spaces, and rejects empty sets. - The npm installer no longer aborts a concurrent first run on Windows. The per-asset cache lock previously treated only `EEXIST` as contention, but a contended `mkdir` on Windows may raise `EPERM` or `EACCES`, so a process diff --git a/scripts/generate_release_checksums.sh b/scripts/generate_release_checksums.sh index a99d96c..cfc57ff 100755 --- a/scripts/generate_release_checksums.sh +++ b/scripts/generate_release_checksums.sh @@ -30,7 +30,7 @@ actual_assets=() while IFS= read -r actual_asset; do actual_assets[${#actual_assets[@]}]="${actual_asset}" done < <( - find "${asset_dir}" -mindepth 1 -maxdepth 1 -type f -exec basename {} + | LC_ALL=C sort + find "${asset_dir}" -mindepth 1 -maxdepth 1 -type f -exec basename {} \; | LC_ALL=C sort ) if [[ "${#actual_assets[@]}" -eq 0 ]] || [[ "${actual_assets[*]}" != "${assets[*]}" ]]; then printf 'ERROR: release assets differ from the exact expected set\n' >&2 diff --git a/scripts/test_release_guards.sh b/scripts/test_release_guards.sh index 4366a82..88cb828 100755 --- a/scripts/test_release_guards.sh +++ b/scripts/test_release_guards.sh @@ -99,6 +99,47 @@ fi expect_failure "version mismatch" \ "${repo_root}/scripts/validate_release_version.sh" 999.999.999 +# Keep CHANGELOG mutations in a fixture tree. All other version surfaces remain +# the real checkout, so failures below must reach the comparison-link guard. +version_fixture="${tmp_dir}/version fixture" +mkdir -p -- "${version_fixture}/scripts" +cp -- "${repo_root}/scripts/validate_release_version.sh" "${version_fixture}/scripts/" +for surface in npm server worker web deploy docs; do + ln -s -- "${repo_root}/${surface}" "${version_fixture}/${surface}" +done +fixture_validator="${version_fixture}/scripts/validate_release_version.sh" +previous_version=0.0.1 + +write_changelog_fixture() { + local link="$1" + local include_previous="${2:-yes}" + { + printf '## [Unreleased]\n\n## [%s] - 2026-01-01\n\n' "${current_version}" + if [[ "${include_previous}" == yes ]]; then + printf '## [%s] - 2025-01-01\n\n' "${previous_version}" + fi + printf '[Unreleased]: https://github.com/bytefolk/mem/compare/v%s...HEAD\n' "${current_version}" + printf '[%s]: https://github.com/bytefolk/mem/%s\n' "${current_version}" "${link}" + } > "${version_fixture}/CHANGELOG.md" +} + +correct_compare="compare/v${previous_version}...${current_tag}" +write_changelog_fixture "${correct_compare}" +"${fixture_validator}" "${current_version}" >/dev/null +for wrong_base in v0.0.0 "${current_tag}" arbitrary; do + write_changelog_fixture "compare/${wrong_base}...${current_tag}" + expect_failure "wrong compare base ${wrong_base}" "${fixture_validator}" "${current_version}" +done +write_changelog_fixture "compare/v${previous_version}...v999.999.999" +expect_failure "wrong compare endpoint" "${fixture_validator}" "${current_version}" +write_changelog_fixture "${correct_compare}/extra" +expect_failure "compare link suffix" "${fixture_validator}" "${current_version}" +write_changelog_fixture "${correct_compare}" no +expect_failure "missing compare predecessor" "${fixture_validator}" "${current_version}" +write_changelog_fixture "releases/tag/${current_tag}" no +"${fixture_validator}" "${current_version}" >/dev/null +printf 'PASS: compare links require the exact predecessor and endpoint; tag links remain valid\n' + notes_file="${tmp_dir}/release-notes.md" "${repo_root}/scripts/render_release_notes.sh" "${current_tag}" > "${notes_file}" [[ -s "${notes_file}" ]] || die "release notes are empty" @@ -190,8 +231,17 @@ expect_failure "annotated tag version mismatch" env \ FAKE_HEAD_COMMIT="${same_commit}" \ "${repo_root}/scripts/validate_release_source.sh" v999.999.999 -asset_dir="${tmp_dir}/assets" +asset_dir="${tmp_dir}/assets with spaces" mkdir -p -- "${asset_dir}" +# An empty set must fail with the intended diagnostic, including on Bash 3.2. +if "${repo_root}/scripts/generate_release_checksums.sh" \ + "${current_tag}" "${same_commit}" "${asset_dir}" > "${tmp_dir}/empty-assets.log" 2>&1; then + die "empty asset directory: command unexpectedly succeeded" +fi +grep -Fq -- 'actual: ' "${tmp_dir}/empty-assets.log" || + die "empty asset directory must report the missing set" +[[ ! -e "${asset_dir}/mem-mcp-checksums.txt" ]] || + die "empty asset directory must not produce a manifest" assets=( mem-mcp-darwin-amd64 mem-mcp-darwin-arm64 @@ -203,6 +253,8 @@ assets=( for asset in "${assets[@]}"; do printf 'test payload for %s\n' "${asset}" > "${asset_dir}/${asset}" done +# Use the real find, basename and sha256sum here. Unlike the Bash-only compat +# suite, this must also catch GNU basename rejecting batched find -exec paths. "${repo_root}/scripts/generate_release_checksums.sh" \ "${current_tag}" "${same_commit}" "${asset_dir}" >/dev/null