You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/09/15 06:14:37 UTC

(opendal) 01/01: fix(release): publish approved source before optional distributions

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch xuanwo/unblock-source-release
in repository https://gitbox.apache.org/repos/asf/opendal.git

commit 662cf31a3642da76e8e93db02cd75a2a64f5acd5
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Tue Sep 15 14:14:21 2026 +0800

    fix(release): publish approved source before optional distributions
---
 .../opendal-release/references/official-release.md |  9 +--
 scripts/release_lifecycle.py                       | 33 +++++++----
 scripts/test_release_lifecycle.py                  | 69 ++++++++++++++++++----
 website/community/release/weekly.md                | 42 +++++++------
 4 files changed, 104 insertions(+), 49 deletions(-)

diff --git a/.agents/skills/opendal-release/references/official-release.md b/.agents/skills/opendal-release/references/official-release.md
index a0e4d7cc72..a749917694 100644
--- a/.agents/skills/opendal-release/references/official-release.md
+++ b/.agents/skills/opendal-release/references/official-release.md
@@ -13,10 +13,11 @@ actor. Existing Nexus credentials must allow promotion.
 
 Final branch and signed tag use the RC branch SHA, cross-checked against its tag.
 Ignore ATR `commit_hash` as the tag target: OIDC can record the main workflow SHA.
-The workflow explicitly dispatches final package jobs, promotes the Java RC's
-closed staging repository, waits for publication, and completes GitHub/ATR
-announcements. It creates a draft main version-sync PR and removes same-version
-RC branches while retaining their tags and the final branch. Review and merge
+The workflow creates the GitHub Release, completes GitHub/ATR announcements and
+opens a draft main version-sync PR before following optional package distribution.
+Package or Nexus failures do not block the source release. It retains RC branches
+for hourly distribution retries, then removes them once follow-up completes,
+while retaining their tags and the final branch. Review and merge
 that PR through normal repository checks; versions must never regress. A PR
 created with `GITHUB_TOKEN` does not start CI: after review, close and reopen it
 with the maintainer's credentials (or push an update) to trigger required checks.
diff --git a/scripts/release_lifecycle.py b/scripts/release_lifecycle.py
index 4ae14f0058..9b74a17609 100644
--- a/scripts/release_lifecycle.py
+++ b/scripts/release_lifecycle.py
@@ -765,15 +765,6 @@ def publish(candidate):
     if not passed(release):
         raise ValueError("ATR has not resolved this vote as passed")
     final_refs(candidate)
-    complete = publish_builds(candidate)
-    java_complete = nexus_release(candidate)
-    if not complete or not java_complete:
-        notice(
-            candidate,
-            release,
-            "Vote passed; publication jobs or Nexus promotion are still running.",
-        )
-        return
     body = announcement(candidate)
     existing = api(f"repos/{REPO}/releases/tags/v{candidate.version}", optional=True)
     if existing and (existing["draft"] or existing["prerelease"]):
@@ -833,8 +824,30 @@ def publish(candidate):
         current["id"],
         f"<!-- opendal-published:{candidate.rc} -->",
         f"[Apache OpenDAL {candidate.version}](https://github.com/{REPO}/releases/tag/v{candidate.version}) is available. "
-        f"[Source branch](https://github.com/{REPO}/tree/releases/{candidate.version}). Version sync: {sync_pr}. RC tags are retained; candidate branches are being removed.",
+        f"[Source branch](https://github.com/{REPO}/tree/releases/{candidate.version}). Version sync: {sync_pr}. RC tags are retained.",
     )
+    # Language distributions are conveniences, not prerequisites for the ASF release.
+    complete = True
+    errors = []
+    for distribute in (publish_builds, nexus_release):
+        try:
+            if not distribute(candidate):
+                complete = False
+        except (RuntimeError, ValueError, OSError) as error:
+            complete = False
+            errors.append(str(error))
+    if not complete:
+        status = (
+            f"Apache OpenDAL {candidate.version} has been released. "
+            "Optional package distribution "
+        )
+        status += (
+            "needs attention: " + "; ".join(errors) if errors else "is still running."
+        )
+        print(status)
+        notice(candidate, release, status)
+        # Discovery uses RC branches; retain them until optional follow-up completes.
+        return
     cleanup(candidate)
 
 
diff --git a/scripts/test_release_lifecycle.py b/scripts/test_release_lifecycle.py
index 04676bdde1..fbba076c5b 100644
--- a/scripts/test_release_lifecycle.py
+++ b/scripts/test_release_lifecycle.py
@@ -141,19 +141,62 @@ class LifecycleTests(unittest.TestCase):
                 release.publish_builds(self.candidate)
             dispatch.assert_not_called()
 
-    def test_waiting_for_packages_does_not_announce_or_cleanup(self):
-        with (
-            patch.object(release.Candidate, "atr", return_value=self.atr),
-            patch.object(release, "final_refs"),
-            patch.object(release, "publish_builds", return_value=False),
-            patch.object(release, "nexus_release", return_value=True),
-            patch.object(release, "notice"),
-            patch.object(release, "api") as api,
-            patch.object(release, "cleanup") as cleanup,
-        ):
-            release.publish(self.candidate)
-            api.assert_not_called()
-            cleanup.assert_not_called()
+    def test_optional_distribution_does_not_block_source_publication(self):
+        for outcome in (False, RuntimeError("Dart publication failed")):
+            with self.subTest(outcome=outcome):
+
+                def distribute(candidate, outcome=outcome):
+                    self.assertIn(
+                        "repos/apache/opendal/releases",
+                        [call.args[0] for call in github.call_args_list],
+                    )
+                    self.assertEqual(
+                        http.call_args.args[0],
+                        f"{release.ATR}/api/publisher/release/announce",
+                    )
+                    sync_pr.assert_called_once()
+                    if isinstance(outcome, Exception):
+                        raise outcome
+                    return outcome
+
+                with (
+                    patch.object(
+                        release.Candidate,
+                        "atr",
+                        side_effect=[self.atr, self.atr | {"phase": "release"}],
+                    ),
+                    patch.object(release, "final_refs"),
+                    patch.object(release, "publish_builds", side_effect=distribute),
+                    patch.object(release, "nexus_release", return_value=True) as nexus,
+                    patch.object(release, "api", return_value=None) as github,
+                    patch.object(release, "version_notes", return_value=""),
+                    patch.object(
+                        release, "request_json", side_effect=[{"value": "jwt"}, {}]
+                    ) as http,
+                    patch.object(release, "discussion"),
+                    patch.object(
+                        release, "sync_versions", return_value="sync-pr"
+                    ) as sync_pr,
+                    patch.object(
+                        release, "notice", return_value={"id": "discussion"}
+                    ) as notice,
+                    patch.object(release, "comment_once"),
+                    patch.object(release, "cleanup") as cleanup,
+                    patch.dict(
+                        os.environ,
+                        {
+                            "ACTIONS_ID_TOKEN_REQUEST_URL": "https://example.invalid/token?request=1",
+                            "ACTIONS_ID_TOKEN_REQUEST_TOKEN": "test",
+                        },
+                    ),
+                ):
+                    release.publish(self.candidate)
+                    nexus.assert_called_once_with(self.candidate)
+                    cleanup.assert_not_called()
+                    self.assertIn("has been released", notice.call_args.args[2])
+                    self.assertIn(
+                        "Optional package distribution", notice.call_args.args[2]
+                    )
 
     def test_announcement_resume_uses_external_completion_records(self):
         atr = self.atr | {"phase": "release"}
diff --git a/website/community/release/weekly.md b/website/community/release/weekly.md
index 93fed2cb7f..a9dce41a1b 100644
--- a/website/community/release/weekly.md
+++ b/website/community/release/weekly.md
@@ -258,33 +258,31 @@ For new `releases/<version>-rc.N` candidates, the hourly workflow calls
    `releases/<version>` branch and signed `v<version>` tag at that commit. Existing
    final refs must agree; they are never moved. ATR's OIDC `commit_hash` is not
    used because the upload workflow starts on main before generating the RC.
-2. Explicitly dispatches the existing Rust, Python, Node.js, Ruby, .NET, Dart and
-   Docs workflows at the final tag, skipping disabled workflows. Ruby accepts
-   final-tag manual dispatch. Publication waits for successful runs; failures
-   require rerunning the failed jobs in their existing run. Dart retains its
-   existing artifact-only behavior. Go belongs to its separate repository.
-3. Promotes the closed Nexus repository from the successful Java RC run, without
-   staging another build, then waits for that Java package version on Maven Central.
-   A disabled Java workflow is skipped like other disabled publishers.
-4. Creates the GitHub Release and asks ATR to send the announcement to
+2. Creates the GitHub Release and asks ATR to send the announcement to
    `announce@apache.org`. ATR checks source publication and download propagation.
-   Posts the same reviewed announcement text in GitHub Announcements, which the
-   repository mirrors to dev@opendal.apache.org.
-5. Opens a draft version-sync PR from the latest main. `update-version --baseline
+   Posts the reviewed announcement in GitHub Announcements, which the repository
+   mirrors to dev@opendal.apache.org.
+3. Opens a draft version-sync PR from the latest main. `update-version --baseline
    v<version> --sync` takes the higher of main and released versions per package,
    preserves new packages and development changes, and regenerates dependencies,
-   lockfiles and the changelog entry. Normal review/CI and merge complete the sync;
-   the workflow does not merge the release branch into main or auto-merge the PR.
-   Because `GITHUB_TOKEN` does not trigger PR CI, a maintainer closes and reopens
-   the reviewed PR (or pushes an update) to start required checks before merging.
-6. Links the release and sync PR in the candidate Discussion, then deletes all
-   `releases/<version>-rc.N` branches for that version. Each deletion requires its
-   RC tag to retain the branch commit. The approved branch is deleted last so a
-   partial cleanup remains discoverable. Final branch, final tag and RC tags stay.
+   lockfiles and the changelog entry. Review and merge complete the sync; the
+   workflow does not merge the release branch or auto-merge the PR. Because
+   `GITHUB_TOKEN` does not trigger PR CI, a maintainer closes and reopens the
+   reviewed PR (or pushes an update) to start checks.
+4. Independently follows optional language distributions: dispatches existing
+   Rust, Python, Node.js, Ruby, .NET, Dart and Docs workflows at the final tag,
+   skipping disabled workflows, and promotes the approved Java Nexus staging
+   repository. Go belongs to its separate repository. Pending or failed package
+   jobs do not block the GitHub Release, announcements or version-sync PR.
+5. Reports optional distribution progress in the candidate Discussion. Retains
+   RC branches while distribution needs follow-up so hourly discovery can resume
+   it. Once complete, removes same-version RC branches; each deletion requires
+   its RC tag to retain the commit. The approved branch is deleted last so partial
+   cleanup remains discoverable. Final branch, final tag and RC tags stay.
 
 Each hourly run checks external completion records and resumes unfinished work.
-Pending packages or Maven propagation defer announcements and cleanup. A failed
-job appears in Actions and in the candidate notice; rerun the failed downstream
+Optional package failures remain visible in their own Actions runs and in the
+candidate notice; the source release is already published. Rerun failed package
 jobs, then wait for the next hourly pass or resume explicitly:
 
 ```bash