From 752e65f075405e092f1ecb0490a58dedb16f20f6 Mon Sep 17 00:00:00 2001 From: "guokexin.02" <264159873+Tantanz20020918@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:01:06 +0800 Subject: [PATCH] fix: tighten release workflow verification --- .github/workflows/release.yml | 4 +++- scripts/release-workflow.test.sh | 36 +++++++++++++++++--------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 131d8042c..bb248aeb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,7 @@ jobs: - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: '22.14.0' + package-manager-cache: false - name: Refuse to rebuild a published release env: @@ -332,7 +333,8 @@ jobs: grep -Eq '^CodeDirectory .*flags=0x[0-9A-Fa-f]+[(][^)]*runtime[^)]*[)]' <<<"$details" grep -Eq '^Timestamp=.+' <<<"$details" codesign --verify --strict --verbose=4 --check-notarization -R='notarized' "$binary" - "$binary" --version | grep -Fq "$VERSION" + escaped_version="${VERSION//./\\.}" + "$binary" --version | grep -Eq "(^|[^0-9A-Za-z.])${escaped_version}([^0-9A-Za-z.]|$)" publish-github: needs: [preflight, create-draft-release, verify-macos] diff --git a/scripts/release-workflow.test.sh b/scripts/release-workflow.test.sh index 8bd671814..4b0639cd4 100755 --- a/scripts/release-workflow.test.sh +++ b/scripts/release-workflow.test.sh @@ -10,13 +10,13 @@ ruby -ropen3 -ryaml <<'RUBY' workflow = YAML.load_file(".github/workflows/release.yml") goreleaser = YAML.load_file(".goreleaser.yml") -def fail(message) +def contract_error(message) abort("release workflow contract: #{message}") end def expect_equal(actual, expected, description) return if actual == expected - fail("#{description}; expected #{expected.inspect}, got #{actual.inspect}") + contract_error("#{description}; expected #{expected.inspect}, got #{actual.inspect}") end def scalar_values(value) @@ -45,7 +45,7 @@ jobs.each do |job_name, job| next unless run.is_a?(String) _stdout, stderr, status = Open3.capture3("bash", "-n", stdin_data: run) - fail("#{job_name}/#{step["name"]} has invalid bash syntax: #{stderr}") unless status.success? + contract_error("#{job_name}/#{step["name"]} has invalid bash syntax: #{stderr}") unless status.success? end end @@ -95,7 +95,7 @@ expected_timeouts.each do |job_name, timeout| end expect_equal(jobs.fetch("build-sign-notarize").fetch("environment"), "npm-production", "signing approval environment") -fail("publish-npm must not request a second Environment approval") if jobs.fetch("publish-npm").key?("environment") +contract_error("publish-npm must not request a second Environment approval") if jobs.fetch("publish-npm").key?("environment") expect_equal(jobs.fetch("publish-npm").fetch("concurrency"), { "group" => "npm-release-${{ needs.preflight.outputs.channel }}", "queue" => "max", @@ -111,7 +111,7 @@ retry_steps = retry_guidance.fetch("steps") expect_equal(retry_steps.length, 1, "number of retry guidance steps") retry_step = retry_steps.first expect_equal(retry_step.fetch("name"), "Write retry guidance", "retry guidance step name") -fail("retry guidance must write to the GitHub step summary") unless retry_step.fetch("run").include?("GITHUB_STEP_SUMMARY") +contract_error("retry guidance must write to the GitHub step summary") unless retry_step.fetch("run").include?("GITHUB_STEP_SUMMARY") signing_references = %w[ secrets.MACOS_SIGN_P12 @@ -146,35 +146,37 @@ expect_equal(macos.fetch("runs-on"), "${{ matrix.runner }}", "macOS matrix runne macos_verify_step = macos.fetch("steps").find { |step| step["name"] == "Verify notarized macOS binary" } macos_verify_run = macos_verify_step&.fetch("run", nil) macos_download_step = macos.fetch("steps").find { |step| step["name"] == "Download release candidate" } -fail("verify-macos must download the build candidate artifact") unless macos_download_step&.fetch("uses", nil)&.start_with?("actions/download-artifact@") -fail("verify-macos must not download mutable Draft Release assets") if macos_verify_run&.include?("gh release download") -fail("verify-macos must verify notarization through codesign") unless macos_verify_run&.include?("--check-notarization -R='notarized'") -fail("verify-macos must detect hardened runtime in CodeDirectory metadata") unless macos_verify_run&.include?("^CodeDirectory .*flags=0x") +contract_error("verify-macos must download the build candidate artifact") unless macos_download_step&.fetch("uses", nil)&.start_with?("actions/download-artifact@") +contract_error("verify-macos must not download mutable Draft Release assets") if macos_verify_run&.include?("gh release download") +contract_error("verify-macos must verify notarization through codesign") unless macos_verify_run&.include?("--check-notarization -R='notarized'") +contract_error("verify-macos must detect hardened runtime in CodeDirectory metadata") unless macos_verify_run&.include?("^CodeDirectory .*flags=0x") +contract_error("verify-macos must match the complete release version") unless macos_verify_run&.include?("escaped_version") draft_step = jobs.fetch("create-draft-release").fetch("steps").find { |step| step["name"] == "Create or reuse Draft Release" } draft_run = draft_step&.fetch("run", nil) -fail("Draft Release creation must write generated release notes") unless draft_run&.include?("--notes-file") -fail("Draft Release reuse must validate target commit and prerelease state") unless draft_run&.include?("targetCommitish") && draft_run.include?("isPrerelease") -fail("Draft Release creation must target the validated source commit") unless draft_run&.include?("--target \"$SOURCE_SHA\"") -fail("Draft Release creation must require the existing remote tag") unless draft_run&.include?("--verify-tag") +contract_error("Draft Release creation must write generated release notes") unless draft_run&.include?("--notes-file") +contract_error("Draft Release reuse must validate target commit and prerelease state") unless draft_run&.include?("targetCommitish") && draft_run.include?("isPrerelease") +contract_error("Draft Release creation must target the validated source commit") unless draft_run&.include?("--target \"$SOURCE_SHA\"") +contract_error("Draft Release creation must require the existing remote tag") unless draft_run&.include?("--verify-tag") github_steps = jobs.fetch("publish-github").fetch("steps") github_check = github_steps.find { |step| step["name"] == "Verify Draft assets match the candidate" } -fail("GitHub publication must verify Draft assets against the candidate") unless github_check&.fetch("run", nil)&.include?("release-candidate/checksums.txt") +contract_error("GitHub publication must verify Draft assets against the candidate") unless github_check&.fetch("run", nil)&.include?("release-candidate/checksums.txt") npm_steps = jobs.fetch("publish-npm").fetch("steps") pinned_npm = npm_steps.find { |step| step["name"] == "Install pinned npm" } -fail("publish-npm must install npm 11.16.0 for trusted publishing") unless pinned_npm&.fetch("run", nil) == "npm install --global npm@11.16.0" +contract_error("publish-npm must install npm 11.16.0 for trusted publishing") unless pinned_npm&.fetch("run", nil) == "npm install --global npm@11.16.0" publish_step = npm_steps.find { |step| step["name"] == "Publish or verify npm package" } -fail("publish-npm must explicitly pass the candidate tarball as a local path") unless publish_step&.fetch("run", nil).include?('npm publish "./$tgz"') +contract_error("publish-npm must explicitly pass the candidate tarball as a local path") unless publish_step&.fetch("run", nil).include?('npm publish "./$tgz"') action_references(workflow).each do |reference| - fail("action is not pinned to a full commit SHA: #{reference}") unless reference.match?(%r{\A[^@]+@[0-9a-f]{40}\z}) + contract_error("action is not pinned to a full commit SHA: #{reference}") unless reference.match?(%r{\A[^@]+@[0-9a-f]{40}\z}) end notarize = goreleaser.fetch("notarize").fetch("macos") expect_equal(notarize.length, 1, "number of macOS notarization configurations") macos_notarize = notarize.first +expect_equal(macos_notarize.fetch("enabled"), '{{ isEnvSet "MACOS_SIGN_P12" }}', "macOS notarization enablement") expect_equal(macos_notarize.fetch("ids"), ["lark-cli"], "notarized build IDs") expect_equal(macos_notarize.fetch("sign"), { "certificate" => "{{ .Env.MACOS_SIGN_P12 }}",