From ed72a522f50ac789a312f24767f2d5a0b0517d6a Mon Sep 17 00:00:00 2001 From: J Date: Thu, 12 Nov 2026 00:58:35 +0530 Subject: [PATCH 1/2] fix(release): use deterministic asset-name ordering in draft verification --- .github/workflows/release-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index b0feeb9cf..eec5c2740 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -242,7 +242,7 @@ jobs: set -euo pipefail . scripts/release/baseline.env names=$(gh release view "v$VERSION" -R "$GITHUB_REPOSITORY" --json assets --jq '[.assets[].name] | sort | join(" ")') - want=$(printf '%s\n' "$SENDER_DMG" "$RECEIVER_DMG" "$SENDER_FEED" "$RECEIVER_FEED" SHA256SUMS.txt | sort | paste -sd' ' -) + want=$(printf '%s\n' "$SENDER_DMG" "$RECEIVER_DMG" "$SENDER_FEED" "$RECEIVER_FEED" SHA256SUMS.txt | LC_ALL=C sort | paste -sd' ' -) [ "$names" = "$want" ] || { echo "::error::draft assets are '$names', expected '$want'"; exit 1; } mkdir check && gh release download "v$VERSION" -R "$GITHUB_REPOSITORY" -D check sed 's# dist/# check/#' expected.sha256 | shasum -a 256 -c - From 6263330395b4cb5816ae1c3e45063c25a71dd8b7 Mon Sep 17 00:00:00 2001 From: raiseCatError <315733358+raiseCatError@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:26:28 +0530 Subject: [PATCH 2/2] test(release): verify deterministic draft asset collation and workflow enforcement --- scripts/release/check-release-workflows.rb | 1 + .../release/test/draft_asset_sorting_test.rb | 43 +++++++++++++++++++ scripts/release/test/workflow_checks_test.rb | 2 + 3 files changed, 46 insertions(+) create mode 100644 scripts/release/test/draft_asset_sorting_test.rb diff --git a/scripts/release/check-release-workflows.rb b/scripts/release/check-release-workflows.rb index 9b5a3310c..b471af827 100755 --- a/scripts/release/check-release-workflows.rb +++ b/scripts/release/check-release-workflows.rb @@ -309,6 +309,7 @@ def release_ref_guard?(step, version_from_input:) err(name, "publish-mac must open the gate (publish start) before publishing") unless gate && first_publish && gate < first_publish err(name, "publish-mac must create the release as a draft first") unless draft && draft == first_publish err(name, "publish-mac must verify the draft's assets before Latest") unless draft && verify && live && draft < verify && verify < live + err(name, "publish-mac must sort expected draft asset names with LC_ALL=C sort") unless s.any? { |x| run_text(x).include?("LC_ALL=C sort") } err(name, "publish-mac must revert Latest when the live feeds don't verify") unless live && revert && live < revert end # The release state is written by GitHub Actions and is not evidence by diff --git a/scripts/release/test/draft_asset_sorting_test.rb b/scripts/release/test/draft_asset_sorting_test.rb new file mode 100644 index 000000000..436015b92 --- /dev/null +++ b/scripts/release/test/draft_asset_sorting_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "open3" + +class DraftAssetSortingTest < Minitest::Test + # The five assets produced by Mac packaging and expected in the release draft: + ASSETS = %w[ + MeowDisplay.dmg + MeowDisplayReceiver.dmg + appcast.xml + appcast-receiver.xml + SHA256SUMS.txt + ].freeze + + def test_lc_all_c_sort_matches_jq_byte_collation + # jq 'sort' orders by Unicode code point / byte value: + # 'M' (0x4D), 'S' (0x53) precede 'a' (0x61). + jq_sorted = %w[ + MeowDisplay.dmg + MeowDisplayReceiver.dmg + SHA256SUMS.txt + appcast-receiver.xml + appcast.xml + ] + + out, status = Open3.capture2({ "LC_ALL" => "C" }, "sort", stdin_data: ASSETS.join("\n") + "\n") + assert status.success? + c_sorted = out.lines.map(&:strip) + + assert_equal jq_sorted, c_sorted + end + + def test_locale_sensitive_sort_divergence_is_documented + # The runner's locale-dependent sort put lowercase 'appcast*' before 'MeowDisplay*'. + # LC_ALL=C is required to guarantee deterministic comparison against jq's output. + c_out, _ = Open3.capture2({ "LC_ALL" => "C" }, "sort", stdin_data: ASSETS.join("\n") + "\n") + c_sorted = c_out.lines.map(&:strip) + + assert_equal "MeowDisplay.dmg", c_sorted.first + assert_equal "appcast.xml", c_sorted.last + end +end diff --git a/scripts/release/test/workflow_checks_test.rb b/scripts/release/test/workflow_checks_test.rb index 77b3904c5..d273d9bd3 100644 --- a/scripts/release/test/workflow_checks_test.rb +++ b/scripts/release/test/workflow_checks_test.rb @@ -119,6 +119,8 @@ def test_an_incomplete_latest_release_is_caught "release-publish.yml" => ['--draft --target "$SHA"', '--target "$SHA"']) assert_caught("publish-mac must verify the draft's assets before Latest", "release-publish.yml" => ['gh release download "v$VERSION"', 'echo download "v$VERSION"']) + assert_caught("publish-mac must sort expected draft asset names with LC_ALL=C sort", + "release-publish.yml" => ['LC_ALL=C sort', 'sort']) assert_caught("publish-mac must revert Latest when the live feeds don't verify", "release-publish.yml" => ['--draft=true', '--draft=false-ish']) assert_caught("publish-mac must open the gate (publish start) before publishing",