diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index b0feeb9c..eec5c274 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 - diff --git a/scripts/release/check-release-workflows.rb b/scripts/release/check-release-workflows.rb index 5cb39e7e..4145bd9f 100644 --- a/scripts/release/check-release-workflows.rb +++ b/scripts/release/check-release-workflows.rb @@ -329,6 +329,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 00000000..436015b9 --- /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 6b9eb31f..59e75f24 100644 --- a/scripts/release/test/workflow_checks_test.rb +++ b/scripts/release/test/workflow_checks_test.rb @@ -134,6 +134,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",