diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08e83fd8..8ee47b14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2645,6 +2645,40 @@ jobs: "$GITHUB_WORKSPACE/tmp/trusted-release-tooling/scripts/release/verify-github-tag-authority.sh" \ "$RELEASE_VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT" + # The sealed candidate tag is intentionally visible while its GitHub + # authority is checked above. Compatibility must instead discover the + # previous delivered stable tag, so hide only this verified candidate + # from this isolated runner's local tag namespace. + - name: Prepare delivered-stable compatibility ref view + if: ${{ matrix.check == 'compatibility' }} + env: + RELEASE_VERSION: ${{ needs.release-contract.outputs.release_version }} + RELEASE_COMMIT: ${{ needs.release-contract.outputs.release_commit }} + RELEASE_TAG_OBJECT: ${{ needs.release-contract.outputs.release_tag_object }} + PREVIOUS_STABLE: ${{ needs.release-contract.outputs.previous_stable }} + PREVIOUS_STABLE_COMMIT: ${{ needs.release-contract.outputs.previous_stable_commit }} + run: | + set -eu + test -n "$RELEASE_VERSION" + test -n "$RELEASE_COMMIT" + test -n "$RELEASE_TAG_OBJECT" + test -n "$PREVIOUS_STABLE" + test -n "$PREVIOUS_STABLE_COMMIT" + test "$RELEASE_VERSION" != "$PREVIOUS_STABLE" + test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT" + test "$(git rev-parse --verify "refs/tags/${RELEASE_VERSION}")" = "$RELEASE_TAG_OBJECT" + test "$(git rev-parse --verify "refs/tags/${RELEASE_VERSION}^{commit}")" = "$RELEASE_COMMIT" + test "$(git rev-parse --verify "${PREVIOUS_STABLE}^{commit}")" = "$PREVIOUS_STABLE_COMMIT" + + git update-ref -d "refs/tags/${RELEASE_VERSION}" "$RELEASE_TAG_OBJECT" + + if git show-ref --verify --quiet "refs/tags/${RELEASE_VERSION}"; then + echo "sealed candidate tag is still visible to compatibility baseline discovery" >&2 + exit 2 + fi + test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT" + test "$(git rev-parse --verify "${PREVIOUS_STABLE}^{commit}")" = "$PREVIOUS_STABLE_COMMIT" + - name: Set up Go uses: actions/setup-go@v5 with: diff --git a/Formula/dingtalk-workspace-cli.rb b/Formula/dingtalk-workspace-cli.rb index fffbf6c0..46d064f7 100644 --- a/Formula/dingtalk-workspace-cli.rb +++ b/Formula/dingtalk-workspace-cli.rb @@ -1,33 +1,33 @@ class DingtalkWorkspaceCli < Formula desc "Automate DingTalk workspace tasks from the terminal" homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli" - version "1.0.57" + version "1.0.58" license "Apache-2.0" on_macos do if Hardware::CPU.arm? - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.57/dws-darwin-arm64.tar.gz" - sha256 "c01c28dc13948a70fca905207073dc8dbd22f7ba7fc90e68b3316eb9a9c98e88" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-darwin-arm64.tar.gz" + sha256 "7d98599f90cae9d42b51ff2863efc87dbfb4a3176ff3c84fc2216110c0157a70" else - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.57/dws-darwin-amd64.tar.gz" - sha256 "d7baa218beefc851c6a933b456055195f8272984ce008d7e0122bdfc5dad94ea" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-darwin-amd64.tar.gz" + sha256 "4c12e35e5bf7e0905812cd42dc94a5345068a2c16e306bb50b13c5c78b5cb95d" end end on_linux do if Hardware::CPU.arm? - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.57/dws-linux-arm64.tar.gz" - sha256 "0bbe9c233a3ff585077bae1ac5000937c32d967846d14cc44c46f98d49b95ae2" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-linux-arm64.tar.gz" + sha256 "5ef6bde24bc3db6a11a0f1d0b3343a048956b2cbcf6cd3409a037fb6ba425489" else - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.57/dws-linux-amd64.tar.gz" - sha256 "f113ce3654f21d1f9ecc7c196f815aeafbca54d377a347b244a15116c5cba698" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-linux-amd64.tar.gz" + sha256 "3ccadcc6f070a39d2b2ba20429a4fcdc2f21639bf79f34361dc7d16f501bfda6" end end resource "skills" do - url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.57/dws-skills.zip" - sha256 "0c9667209cf30761427a8f9348149cbbf1e397aa3c25587e99f205bc7525e101" + url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.58/dws-skills.zip" + sha256 "2626debc21c3daadfd155b4c167b2219b97e801398fe4441a8b48138960ab264" end def install diff --git a/test/scripts/package_script_test.go b/test/scripts/package_script_test.go index f0f640f8..cc43e8cf 100644 --- a/test/scripts/package_script_test.go +++ b/test/scripts/package_script_test.go @@ -1323,6 +1323,136 @@ func TestReleaseWorkflowParallelizesSealedValidationWithoutWeakeningPublication( } } +func TestReleaseWorkflowHidesOnlyVerifiedSealedTagFromCompatibilityBaseline(t *testing.T) { + t.Parallel() + workflow := readReleaseWorkflow(t) + validation := releaseWorkflowSection(t, workflow, " release-validation:\n", "\n release-plan:\n") + preparedView := releaseWorkflowSection( + t, + validation, + " - name: Prepare delivered-stable compatibility ref view\n", + "\n - name: Set up Go\n", + ) + script := releaseWorkflowRunScript( + t, + validation, + "Prepare delivered-stable compatibility ref view", + "Set up Go", + ) + + if !strings.Contains(preparedView, "if: ${{ matrix.check == 'compatibility' }}") { + t.Fatal("delivered-stable ref view must run only for the compatibility matrix leg") + } + for _, required := range []string{ + "RELEASE_VERSION: ${{ needs.release-contract.outputs.release_version }}", + "RELEASE_COMMIT: ${{ needs.release-contract.outputs.release_commit }}", + "RELEASE_TAG_OBJECT: ${{ needs.release-contract.outputs.release_tag_object }}", + "PREVIOUS_STABLE: ${{ needs.release-contract.outputs.previous_stable }}", + "PREVIOUS_STABLE_COMMIT: ${{ needs.release-contract.outputs.previous_stable_commit }}", + } { + if !strings.Contains(preparedView, required) { + t.Errorf("delivered-stable compatibility view is missing %q", required) + } + } + for _, required := range []string{ + `test "$RELEASE_VERSION" != "$PREVIOUS_STABLE"`, + `test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT"`, + `test "$(git rev-parse --verify "refs/tags/${RELEASE_VERSION}")" = "$RELEASE_TAG_OBJECT"`, + `test "$(git rev-parse --verify "refs/tags/${RELEASE_VERSION}^{commit}")" = "$RELEASE_COMMIT"`, + `test "$(git rev-parse --verify "${PREVIOUS_STABLE}^{commit}")" = "$PREVIOUS_STABLE_COMMIT"`, + `git update-ref -d "refs/tags/${RELEASE_VERSION}" "$RELEASE_TAG_OBJECT"`, + `git show-ref --verify --quiet "refs/tags/${RELEASE_VERSION}"`, + } { + if !strings.Contains(script, required) { + t.Errorf("delivered-stable compatibility view is missing %q", required) + } + } + + verifiedTag := strings.Index(validation, "verify-github-tag-authority.sh") + deleteLocalTag := strings.Index(validation, "git update-ref -d") + compatibility := strings.Index(validation, "check-command-compatibility.sh") + if verifiedTag == -1 || deleteLocalTag == -1 || compatibility == -1 || + verifiedTag > deleteLocalTag || deleteLocalTag > compatibility { + t.Fatal("release tag authority verification, local candidate removal, and compatibility checking must stay ordered") + } + for _, forbidden := range []string{"git push --delete", "github.rest.git.deleteRef"} { + if strings.Contains(workflow, forbidden) { + t.Errorf("release workflow must not delete a remote ref: found %q", forbidden) + } + } + + repo, err := os.MkdirTemp(".", ".release-compatibility-test-") + if err != nil { + t.Fatalf("MkdirTemp(release compatibility test repository) error = %v", err) + } + t.Cleanup(func() { + if err := os.RemoveAll(repo); err != nil { + t.Errorf("RemoveAll(%s) error = %v", repo, err) + } + }) + mustRun(t, repo, "git", "init", "-b", "main") + mustRun(t, repo, "git", "config", "user.name", "Release Compatibility Test") + mustRun(t, repo, "git", "config", "user.email", "release-compatibility@example.com") + mustWriteFile(t, filepath.Join(repo, "tracked"), []byte("stable\n"), 0o644) + mustRun(t, repo, "git", "add", "tracked") + mustRun(t, repo, "git", "commit", "-m", "stable release") + previousStableCommit := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "HEAD")) + mustRun(t, repo, "git", "tag", "-a", "v1.0.57", "-m", "v1.0.57") + mustWriteFile(t, filepath.Join(repo, "tracked"), []byte("sealed\n"), 0o644) + mustRun(t, repo, "git", "commit", "-am", "sealed release") + releaseCommit := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "HEAD")) + mustRun(t, repo, "git", "tag", "-a", "v1.0.58", "-m", "v1.0.58") + releaseTagObject := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "refs/tags/v1.0.58")) + + run := func(tagObject string) ([]byte, error) { + cmd := exec.Command("sh", "-c", script) + cmd.Dir = repo + cmd.Env = make([]string, 0, len(os.Environ())+5) + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "RELEASE_VERSION=") || + strings.HasPrefix(entry, "RELEASE_COMMIT=") || + strings.HasPrefix(entry, "RELEASE_TAG_OBJECT=") || + strings.HasPrefix(entry, "PREVIOUS_STABLE=") || + strings.HasPrefix(entry, "PREVIOUS_STABLE_COMMIT=") { + continue + } + cmd.Env = append(cmd.Env, entry) + } + cmd.Env = append(cmd.Env, + "RELEASE_VERSION=v1.0.58", + "RELEASE_COMMIT="+releaseCommit, + "RELEASE_TAG_OBJECT="+tagObject, + "PREVIOUS_STABLE=v1.0.57", + "PREVIOUS_STABLE_COMMIT="+previousStableCommit, + ) + return cmd.CombinedOutput() + } + + if output, err := run(releaseTagObject); err != nil { + t.Fatalf("prepared compatibility ref view failed: %v\noutput:\n%s", err, output) + } + candidateTag := exec.Command("git", "rev-parse", "--verify", "refs/tags/v1.0.58") + candidateTag.Dir = repo + if _, err := candidateTag.CombinedOutput(); err == nil { + t.Fatal("prepared compatibility ref view left the sealed candidate tag visible") + } + mustRun(t, repo, "git", "cat-file", "-e", releaseTagObject+"^{tag}") + if got := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "refs/tags/v1.0.57^{commit}")); got != previousStableCommit { + t.Fatalf("previous stable tag commit = %s, want %s", got, previousStableCommit) + } + if got := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "HEAD")); got != releaseCommit { + t.Fatalf("HEAD after prepared compatibility ref view = %s, want %s", got, releaseCommit) + } + + mustRun(t, repo, "git", "update-ref", "refs/tags/v1.0.58", releaseTagObject) + if output, err := run(strings.Repeat("0", 40)); err == nil { + t.Fatalf("prepared compatibility ref view unexpectedly accepted a mismatched tag object\noutput:\n%s", output) + } + if got := strings.TrimSpace(mustOutput(t, repo, "git", "rev-parse", "refs/tags/v1.0.58")); got != releaseTagObject { + t.Fatalf("mismatched tag object removed or changed the local sealed tag: got %s, want %s", got, releaseTagObject) + } +} + func TestReleaseFingerprintRefPatternsMatchAllAllocatedTags(t *testing.T) { t.Parallel() repo := t.TempDir()