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

mfordjody pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new 8b15fb9f Remove outdated product (#919)
8b15fb9f is described below

commit 8b15fb9f1fec22df4b1425958ede35d8bdc87acc
Author: mfordjody <[email protected]>
AuthorDate: Thu Jun 18 22:32:22 2026 +0800

    Remove outdated product (#919)
    
    * delete logo
    
    * Update moviereview sample
    
    * add dubbo mesh cni
    
    * Remove outdated product
---
 dubbocni/main.go                                   | 139 +++++++
 tests/e2e/dubbod-observability.sh                  | 100 -----
 tests/e2e/dubbod-xds-dxload.sh                     |  84 -----
 tests/e2e/multicluster-primary-remote.sh           | 122 -------
 tools/release_notes/generate_release_notes.py      | 401 ---------------------
 tools/release_notes/generate_release_notes_test.py |  86 -----
 6 files changed, 139 insertions(+), 793 deletions(-)

diff --git a/dubbocni/main.go b/dubbocni/main.go
new file mode 100644
index 00000000..7e512f4e
--- /dev/null
+++ b/dubbocni/main.go
@@ -0,0 +1,139 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+       "context"
+       "flag"
+       "fmt"
+       "io"
+       "os"
+       "os/signal"
+       "syscall"
+       "time"
+
+       "github.com/apache/dubbo-kubernetes/pkg/cni"
+)
+
+func main() {
+       if len(os.Args) > 1 {
+               switch os.Args[1] {
+               case "install":
+                       if err := runInstall(os.Args[2:]); err != nil {
+                               exitErr(err)
+                       }
+                       return
+               case "uninstall":
+                       if err := runUninstall(os.Args[2:]); err != nil {
+                               exitErr(err)
+                       }
+                       return
+               }
+       }
+
+       stdin, err := io.ReadAll(os.Stdin)
+       if err != nil {
+               exitErr(fmt.Errorf("read stdin: %w", err))
+       }
+
+       conf, err := cni.ParseNetConf(stdin)
+       if err != nil {
+               exitErr(err)
+       }
+       env := cni.EnvFromOS()
+       if env.Command == "VERSION" {
+               out, err := cni.Plugin{}.Run(context.Background(), env, conf)
+               if err != nil {
+                       exitErr(err)
+               }
+               if _, err := os.Stdout.Write(out); err != nil {
+                       exitErr(fmt.Errorf("write stdout: %w", err))
+               }
+               return
+       }
+
+       plugin := cni.Plugin{
+               RuleManager: cni.NewIPTablesRuleManager(conf),
+               StateStore:  cni.NewFileStateStore(conf.StateDirectory()),
+       }
+       if (env.Command == "ADD" || env.Command == "CHECK") && 
cni.EnvHasKubernetesPod(env) {
+               provider, err := 
cni.NewKubernetesPodInfoProvider(conf.KubeConfigPath())
+               if err != nil {
+                       exitErr(err)
+               }
+               plugin.PodInfoProvider = provider
+       }
+       out, err := plugin.Run(context.Background(), env, conf)
+       if err != nil {
+               exitErr(err)
+       }
+       if len(out) > 0 {
+               if _, err := os.Stdout.Write(out); err != nil {
+                       exitErr(fmt.Errorf("write stdout: %w", err))
+               }
+       }
+}
+
+func runInstall(args []string) error {
+       opts := cni.DefaultInstallerOptions()
+       var watch bool
+       var interval time.Duration
+       flags := installerFlagSet("install", &opts)
+       flags.BoolVar(&watch, "watch", false, "keep installer running and 
refresh service account credentials")
+       flags.DurationVar(&interval, "refresh-interval", 
cni.DefaultInstallerInterval(), "credential refresh interval")
+       if err := flags.Parse(args); err != nil {
+               return err
+       }
+       ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, 
syscall.SIGTERM)
+       defer stop()
+       if watch {
+               return cni.InstallLoop(ctx, opts, interval)
+       }
+       return cni.Install(ctx, opts)
+}
+
+func runUninstall(args []string) error {
+       opts := cni.DefaultInstallerOptions()
+       flags := installerFlagSet("uninstall", &opts)
+       if err := flags.Parse(args); err != nil {
+               return err
+       }
+       return cni.Uninstall(opts)
+}
+
+func installerFlagSet(name string, opts *cni.InstallerOptions) *flag.FlagSet {
+       flags := flag.NewFlagSet(name, flag.ContinueOnError)
+       flags.StringVar(&opts.BinDir, "bin-dir", opts.BinDir, "host CNI binary 
directory")
+       flags.StringVar(&opts.ConfDir, "conf-dir", opts.ConfDir, "host CNI 
config directory")
+       flags.StringVar(&opts.StateDir, "state-dir", opts.StateDir, "host state 
directory")
+       flags.StringVar(&opts.KubeConfigPath, "kubeconfig", 
opts.KubeConfigPath, "host kubeconfig path used by the CNI plugin")
+       flags.StringVar(&opts.TokenFile, "token-file", opts.TokenFile, "host 
service account token file")
+       flags.StringVar(&opts.CAFile, "ca-file", opts.CAFile, "host Kubernetes 
CA file")
+       flags.StringVar(&opts.ServiceAccountTokenPath, "service-account-token", 
opts.ServiceAccountTokenPath, "mounted service account token source")
+       flags.StringVar(&opts.ServiceAccountCAPath, "service-account-ca", 
opts.ServiceAccountCAPath, "mounted service account CA source")
+       flags.StringVar(&opts.APIServer, "api-server", opts.APIServer, 
"Kubernetes API server URL")
+       flags.StringVar(&opts.ManagedLabel, "managed-label", opts.ManagedLabel, 
"label key that marks mesh-managed Pods")
+       flags.StringVar(&opts.ManagedLabelValue, "managed-label-value", 
opts.ManagedLabelValue, "label value that marks mesh-managed Pods")
+       flags.StringVar(&opts.IPTablesPath, "iptables-path", opts.IPTablesPath, 
"iptables binary used by the CNI plugin")
+       flags.StringVar(&opts.IPSetPath, "ipset-path", opts.IPSetPath, "ipset 
binary used by the CNI plugin")
+       flags.IntVar(&opts.XServerPort, "xserver-port", opts.XServerPort, 
"xserver inbound port")
+       return flags
+}
+
+func exitErr(err error) {
+       fmt.Fprintln(os.Stderr, err)
+       os.Exit(1)
+}
diff --git a/tests/e2e/dubbod-observability.sh 
b/tests/e2e/dubbod-observability.sh
deleted file mode 100755
index d3ef0b1d..00000000
--- a/tests/e2e/dubbod-observability.sh
+++ /dev/null
@@ -1,100 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -euo pipefail
-
-KUBECTL="${KUBECTL:-kubectl}"
-DUBBOD_NAMESPACE="${DUBBOD_NAMESPACE:-dubbo-system}"
-DUBBOD_SERVICE="${DUBBOD_SERVICE:-dubbod}"
-DUBBOD_DEPLOYMENT="${DUBBOD_DEPLOYMENT:-dubbod}"
-ADDONS_DIR="${ADDONS_DIR:-samples/addons}"
-PROMETHEUS_SERVICE="${PROMETHEUS_SERVICE:-prometheus}"
-GRAFANA_SERVICE="${GRAFANA_SERVICE:-grafana}"
-PROMETHEUS_LOCAL_PORT="${PROMETHEUS_LOCAL_PORT:-19090}"
-PROMETHEUS_QUERY="${PROMETHEUS_QUERY:-up{job=\"dubbod\"}}"
-DRY_RUN_ONLY="${DUBBO_OBSERVABILITY_DRY_RUN_ONLY:-false}"
-
-if [[ ! -d "${ADDONS_DIR}" ]]; then
-  echo "addons directory not found: ${ADDONS_DIR}" >&2
-  exit 1
-fi
-
-"${KUBECTL}" apply --dry-run=client -f "${ADDONS_DIR}" >/dev/null
-
-if [[ "${DRY_RUN_ONLY}" == "true" ]]; then
-  exit 0
-fi
-
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" rollout status 
"deploy/${DUBBOD_DEPLOYMENT}" --timeout=180s
-"${KUBECTL}" apply -f "${ADDONS_DIR}"
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" rollout status 
"deploy/${PROMETHEUS_SERVICE}" --timeout=180s
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" rollout status 
"deploy/${GRAFANA_SERVICE}" --timeout=180s
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" get "svc/${PROMETHEUS_SERVICE}" 
"svc/${GRAFANA_SERVICE}" >/dev/null
-
-scrape_annotation="$("${KUBECTL}" -n "${DUBBOD_NAMESPACE}" get 
"svc/${DUBBOD_SERVICE}" -o 
jsonpath='{.metadata.annotations.prometheus\.io/scrape}')"
-metrics_port="$("${KUBECTL}" -n "${DUBBOD_NAMESPACE}" get 
"svc/${DUBBOD_SERVICE}" -o 
jsonpath='{.metadata.annotations.prometheus\.io/port}')"
-if [[ "${scrape_annotation}" != "true" || "${metrics_port}" != "8080" ]]; then
-  echo "dubbod service prometheus annotations invalid: 
scrape=${scrape_annotation} port=${metrics_port}" >&2
-  exit 1
-fi
-
-pf_log="$(mktemp -t dubbod-observability-prometheus.XXXXXX.log)"
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" port-forward "svc/${PROMETHEUS_SERVICE}" 
\
-  "${PROMETHEUS_LOCAL_PORT}:9090" >"${pf_log}" 2>&1 &
-pf_pid="$!"
-
-cleanup() {
-  if kill -0 "${pf_pid}" >/dev/null 2>&1; then
-    kill "${pf_pid}" >/dev/null 2>&1 || true
-    wait "${pf_pid}" >/dev/null 2>&1 || true
-  fi
-  rm -f "${pf_log}"
-}
-trap cleanup EXIT
-
-for _ in $(seq 1 100); do
-  if nc -z 127.0.0.1 "${PROMETHEUS_LOCAL_PORT}" >/dev/null 2>&1; then
-    break
-  fi
-  if ! kill -0 "${pf_pid}" >/dev/null 2>&1; then
-    cat "${pf_log}" >&2
-    exit 1
-  fi
-  sleep 0.1
-done
-
-if ! nc -z 127.0.0.1 "${PROMETHEUS_LOCAL_PORT}" >/dev/null 2>&1; then
-  cat "${pf_log}" >&2
-  echo "prometheus port-forward did not become ready" >&2
-  exit 1
-fi
-
-python3 - "${PROMETHEUS_LOCAL_PORT}" "${PROMETHEUS_QUERY}" <<'PY'
-import json
-import sys
-import urllib.parse
-import urllib.request
-
-port, query = sys.argv[1], sys.argv[2]
-url = f"http://127.0.0.1:{port}/api/v1/query?"; + 
urllib.parse.urlencode({"query": query})
-with urllib.request.urlopen(url, timeout=10) as response:
-    payload = json.load(response)
-
-if payload.get("status") != "success":
-    raise SystemExit(f"prometheus query failed: {payload}")
-if not payload.get("data", {}).get("result"):
-    raise SystemExit(f"prometheus query returned no series: {query}")
-PY
diff --git a/tests/e2e/dubbod-xds-dxload.sh b/tests/e2e/dubbod-xds-dxload.sh
deleted file mode 100755
index 7f232f65..00000000
--- a/tests/e2e/dubbod-xds-dxload.sh
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -euo pipefail
-
-KUBECTL="${KUBECTL:-kubectl}"
-DUBBOD_NAMESPACE="${DUBBOD_NAMESPACE:-dubbo-system}"
-DUBBOD_SERVICE="${DUBBOD_SERVICE:-dubbod}"
-DUBBOD_DEPLOYMENT="${DUBBOD_DEPLOYMENT:-dubbod}"
-DUBBOD_XDS_PORT="${DUBBOD_XDS_PORT:-26010}"
-DUBBOD_LOCAL_PORT="${DUBBOD_LOCAL_PORT:-26010}"
-DXLOAD_DIR="${DXLOAD_DIR:-/Users/mfordjody/laboratory/opensource/dxload}"
-DXLOAD_CONFIG="${DXLOAD_CONFIG:-${DXLOAD_DIR}/examples/basic.yaml}"
-DXLOAD_DURATION="${DXLOAD_DURATION:-20s}"
-DXLOAD_METRICS_PORT="${DXLOAD_METRICS_PORT:-127.0.0.1:0}"
-
-if [[ ! -d "${DXLOAD_DIR}" ]]; then
-  echo "dxload directory not found: ${DXLOAD_DIR}" >&2
-  exit 1
-fi
-if [[ ! -f "${DXLOAD_CONFIG}" ]]; then
-  echo "dxload config not found: ${DXLOAD_CONFIG}" >&2
-  exit 1
-fi
-
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" rollout status 
"deploy/${DUBBOD_DEPLOYMENT}" --timeout=120s
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" get "svc/${DUBBOD_SERVICE}" >/dev/null
-
-pf_log="$(mktemp -t dubbod-xds-port-forward.XXXXXX.log)"
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" port-forward "svc/${DUBBOD_SERVICE}" \
-  "${DUBBOD_LOCAL_PORT}:${DUBBOD_XDS_PORT}" >"${pf_log}" 2>&1 &
-pf_pid="$!"
-
-cleanup() {
-  if kill -0 "${pf_pid}" >/dev/null 2>&1; then
-    kill "${pf_pid}" >/dev/null 2>&1 || true
-    wait "${pf_pid}" >/dev/null 2>&1 || true
-  fi
-  rm -f "${pf_log}"
-}
-trap cleanup EXIT
-
-for _ in $(seq 1 100); do
-  if nc -z 127.0.0.1 "${DUBBOD_LOCAL_PORT}" >/dev/null 2>&1; then
-    break
-  fi
-  if ! kill -0 "${pf_pid}" >/dev/null 2>&1; then
-    cat "${pf_log}" >&2
-    exit 1
-  fi
-  sleep 0.1
-done
-
-if ! nc -z 127.0.0.1 "${DUBBOD_LOCAL_PORT}" >/dev/null 2>&1; then
-  cat "${pf_log}" >&2
-  echo "dubbod xDS port-forward did not become ready" >&2
-  exit 1
-fi
-
-(
-  cd "${DXLOAD_DIR}"
-  go run . cluster \
-    --address "127.0.0.1:${DUBBOD_LOCAL_PORT}" \
-    --config "${DXLOAD_CONFIG}" \
-    --duration "${DXLOAD_DURATION}" \
-    --metrics-port "${DXLOAD_METRICS_PORT}" \
-    --fail-on-errors=true \
-    --require-responses=true
-)
-
-"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" logs "deploy/${DUBBOD_DEPLOYMENT}" 
--since=2m | grep -E "new connection for node|XDS: Pushing|Push Status" 
>/dev/null
diff --git a/tests/e2e/multicluster-primary-remote.sh 
b/tests/e2e/multicluster-primary-remote.sh
deleted file mode 100755
index 5622ce1b..00000000
--- a/tests/e2e/multicluster-primary-remote.sh
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/usr/bin/env bash
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -euo pipefail
-
-KUBECTL="${KUBECTL:-kubectl}"
-DUBBOCTL="${DUBBOCTL:-go run ./dubboctl}"
-PRIMARY_CONTEXT="${PRIMARY_CONTEXT:?PRIMARY_CONTEXT is required}"
-REMOTE_CONTEXT="${REMOTE_CONTEXT:?REMOTE_CONTEXT is required}"
-PRIMARY_KUBECONFIG="${PRIMARY_KUBECONFIG:-${KUBECONFIG:-${HOME}/.kube/config}}"
-REMOTE_CLUSTER_NAME="${REMOTE_CLUSTER_NAME:?REMOTE_CLUSTER_NAME is required}"
-REMOTE_WEBHOOK_URL="${REMOTE_WEBHOOK_URL:?REMOTE_WEBHOOK_URL is required}"
-REMOTE_XDS_ADDRESS="${REMOTE_XDS_ADDRESS:?REMOTE_XDS_ADDRESS is required}"
-REMOTE_CA_ADDRESS="${REMOTE_CA_ADDRESS:?REMOTE_CA_ADDRESS is required}"
-CA_BUNDLE_FILE="${CA_BUNDLE_FILE:?CA_BUNDLE_FILE is required}"
-REMOTE_KUBECONFIG="${REMOTE_KUBECONFIG:-${KUBECONFIG:-${HOME}/.kube/config}}"
-MULTICLUSTER_NETWORK_MODE="${MULTICLUSTER_NETWORK_MODE:-cross-network}"
-PRIMARY_CLUSTER_NAME="${PRIMARY_CLUSTER_NAME:-Kubernetes}"
-EASTWEST_GATEWAY_PORT="${EASTWEST_GATEWAY_PORT:-15443}"
-EASTWEST_SERVICE_TYPE="${EASTWEST_SERVICE_TYPE:-NodePort}"
-EASTWEST_NODE_PORT="${EASTWEST_NODE_PORT:-32443}"
-REMOTE_EASTWEST_XDS_ADDRESS="${REMOTE_EASTWEST_XDS_ADDRESS:-}"
-
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" cluster-info >/dev/null
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
cluster-info >/dev/null
-
-if [[ "${MULTICLUSTER_NETWORK_MODE}" == "same-network" ]]; then
-  primary_pod_cidrs="$("${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" 
--context "${PRIMARY_CONTEXT}" get nodes \
-    -o jsonpath='{range .items[*]}{.spec.podCIDR}{"\n"}{end}' | sort -u)"
-  remote_pod_cidrs="$("${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" 
--context "${REMOTE_CONTEXT}" get nodes \
-    -o jsonpath='{range .items[*]}{.spec.podCIDR}{"\n"}{end}' | sort -u)"
-  if comm -12 <(printf '%s\n' "${primary_pod_cidrs}") <(printf '%s\n' 
"${remote_pod_cidrs}") | grep -q .; then
-    echo "primary and remote pod CIDRs overlap; same-network multicluster 
requires disjoint, routable Pod CIDRs" >&2
-    exit 1
-  fi
-else
-  primary_gateways="$("${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" 
--context "${PRIMARY_CONTEXT}" -n dubbo-system get deploy dubbod \
-    -o 
jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="DUBBO_EASTWEST_GATEWAYS")].value}')"
-  if [[ "${primary_gateways}" != *"${REMOTE_CLUSTER_NAME}="* || 
"${primary_gateways}" != *"${PRIMARY_CLUSTER_NAME}="* ]]; then
-    echo "dubbod DUBBO_EASTWEST_GATEWAYS=${primary_gateways}; cross-network 
requires ${REMOTE_CLUSTER_NAME}=... and ${PRIMARY_CLUSTER_NAME}=..." >&2
-    exit 1
-  fi
-  if [[ -z "${REMOTE_EASTWEST_XDS_ADDRESS}" ]]; then
-    echo "REMOTE_EASTWEST_XDS_ADDRESS is required for cross-network remote 
dxgate bootstrap" >&2
-    exit 1
-  fi
-fi
-
-${DUBBOCTL} multicluster create-remote-secret \
-  --cluster-name "${REMOTE_CLUSTER_NAME}" \
-  --kubeconfig "${REMOTE_KUBECONFIG}" \
-  --context "${REMOTE_CONTEXT}" | "${KUBECTL}" --kubeconfig 
"${PRIMARY_KUBECONFIG}" --context "${PRIMARY_CONTEXT}" apply -f -
-
-${DUBBOCTL} multicluster generate-remote-manifest \
-  --cluster-name "${REMOTE_CLUSTER_NAME}" \
-  --webhook-url "${REMOTE_WEBHOOK_URL}" \
-  --xds-address "${REMOTE_XDS_ADDRESS}" \
-  --ca-address "${REMOTE_CA_ADDRESS}" \
-  --ca-bundle-file "${CA_BUNDLE_FILE}" | "${KUBECTL}" --kubeconfig 
"${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" apply -f -
-
-if [[ "${MULTICLUSTER_NETWORK_MODE}" != "same-network" ]]; then
-  gateway_args=(
-    multicluster generate-eastwest-gateway
-    --xds-address "${REMOTE_EASTWEST_XDS_ADDRESS}"
-    --service-type "${EASTWEST_SERVICE_TYPE}"
-    --port "${EASTWEST_GATEWAY_PORT}"
-  )
-  if [[ "${EASTWEST_NODE_PORT}" != "0" ]]; then
-    gateway_args+=(--node-port "${EASTWEST_NODE_PORT}")
-  fi
-  ${DUBBOCTL} "${gateway_args[@]}" | "${KUBECTL}" --kubeconfig 
"${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" apply -f -
-fi
-
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" create namespace app --dry-run=client -o yaml | \
-  "${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" apply -f -
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" label namespace app dubbo-injection=enabled --overwrite
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" apply -f samples/app/deployment.yaml
-
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
create namespace app --dry-run=client -o yaml | \
-  "${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context 
"${REMOTE_CONTEXT}" apply -f -
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
label namespace app dubbo-injection=enabled --overwrite
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
apply -f samples/app/deployment.yaml
-
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" -n app scale deploy/nginx-v2 --replicas=0
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
-n app scale deploy/nginx-v1 --replicas=0
-
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" -n app rollout status deploy/nginx-v1 --timeout=180s
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" -n app rollout status deploy/nginx-v2 --timeout=180s
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" -n app rollout status deploy/nginx-consumer --timeout=180s
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
-n app rollout status deploy/nginx-v1 --timeout=180s
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
-n app rollout status deploy/nginx-v2 --timeout=180s
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
-n app rollout status deploy/nginx-consumer --timeout=180s
-
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" apply -f samples/app/meshservice.yaml
-if [[ "${MULTICLUSTER_NETWORK_MODE}" != "same-network" ]]; then
-  "${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" apply -f samples/multicluster/eastwest-nginx-httproute.yaml
-fi
-
-remote_cluster_id="$("${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" 
--context "${REMOTE_CONTEXT}" -n app get pod -l app=nginx-consumer \
-  -o 
jsonpath='{.items[0].spec.containers[0].env[?(@.name=="DUBBO_META_CLUSTER_ID")].value}')"
-if [[ "${remote_cluster_id}" != "${REMOTE_CLUSTER_NAME}" ]]; then
-  echo "remote consumer cluster id = ${remote_cluster_id}, want 
${REMOTE_CLUSTER_NAME}" >&2
-  exit 1
-fi
-
-"${KUBECTL}" --kubeconfig "${PRIMARY_KUBECONFIG}" --context 
"${PRIMARY_CONTEXT}" -n app exec deploy/nginx-consumer -- \
-  dubbod xclient --expect v1=50,v2=50 100 | sort | uniq -c
-"${KUBECTL}" --kubeconfig "${REMOTE_KUBECONFIG}" --context "${REMOTE_CONTEXT}" 
-n app exec deploy/nginx-consumer -- \
-  dubbod xclient --expect v1=50,v2=50 100 | sort | uniq -c
diff --git a/tools/release_notes/generate_release_notes.py 
b/tools/release_notes/generate_release_notes.py
deleted file mode 100644
index 1db7afbd..00000000
--- a/tools/release_notes/generate_release_notes.py
+++ /dev/null
@@ -1,401 +0,0 @@
-#!/usr/bin/env python3
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from __future__ import annotations
-
-import argparse
-import dataclasses
-import datetime as dt
-import os
-import re
-import subprocess
-from pathlib import Path
-from typing import Iterable
-
-
-VERSION_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$")
-PR_RE = re.compile(r"\(#(?P<num>\d+)\)")
-
-
[email protected](frozen=True)
-class Commit:
-    subject: str
-    sha: str
-    author: str
-
-
[email protected](frozen=True)
-class ReleaseContext:
-    tag: str
-    version: str
-    previous_tag: str | None
-    repo: str
-    date: dt.date
-
-
-CATEGORY_ORDER = [
-    ("security", "Security", "安全"),
-    ("features", "Features", "新增功能"),
-    ("traffic", "Traffic Management", "流量管理"),
-    ("installation", "Installation and CLI", "安装与命令行"),
-    ("fixes", "Bug Fixes", "问题修复"),
-    ("tests", "Tests", "测试"),
-    ("docs", "Documentation", "文档"),
-    ("other", "Other Changes", "其他变更"),
-]
-
-
-def run_git(args: list[str], cwd: Path) -> str:
-    return subprocess.check_output(["git", *args], cwd=cwd, text=True).strip()
-
-
-def parse_version(tag: str) -> tuple[int, int, int]:
-    match = VERSION_RE.match(strip_ref(tag))
-    if not match:
-        raise ValueError(f"tag must be a semantic version such as 0.4.0 or 
v0.4.0: {tag}")
-    return tuple(int(part) for part in match.groups())
-
-
-def strip_ref(tag: str) -> str:
-    return tag.removeprefix("refs/tags/")
-
-
-def display_version(tag: str) -> str:
-    return strip_ref(tag).removeprefix("v")
-
-
-def minor_series(version: str) -> str:
-    major, minor, _ = parse_version(version)
-    return f"{major}.{minor}.x"
-
-
-def english_date(value: dt.date) -> str:
-    months = [
-        "Jan",
-        "Feb",
-        "Mar",
-        "Apr",
-        "May",
-        "Jun",
-        "Jul",
-        "Aug",
-        "Sep",
-        "Oct",
-        "Nov",
-        "Dec",
-    ]
-    return f"{months[value.month - 1]} {value.day}, {value.year}"
-
-
-def release_kind(version: str) -> tuple[str, str]:
-    _, _, patch = parse_version(version)
-    if patch == 0:
-        return "minor release", "小版本发布"
-    return "patch release", "补丁版本发布"
-
-
-def source_changes_url(ctx: ReleaseContext) -> str:
-    base = f"https://github.com/{ctx.repo}";
-    if ctx.previous_tag:
-        return f"{base}/compare/{ctx.previous_tag}...{ctx.tag}"
-    return f"{base}/commits/{ctx.tag}"
-
-
-def github_release_url(ctx: ReleaseContext) -> str:
-    return f"https://github.com/{ctx.repo}/releases/tag/{ctx.tag}";
-
-
-def previous_tag_for(tag: str, cwd: Path) -> str | None:
-    current = parse_version(tag)
-    tags = []
-    for candidate in run_git(["tag", "--list"], cwd).splitlines():
-        try:
-            parsed = parse_version(candidate)
-        except ValueError:
-            continue
-        if parsed < current:
-            tags.append((parsed, candidate))
-    if not tags:
-        return None
-    tags.sort()
-    return tags[-1][1]
-
-
-def git_log_range(ctx: ReleaseContext) -> str:
-    if ctx.previous_tag:
-        return f"{ctx.previous_tag}..{ctx.tag}"
-    return ctx.tag
-
-
-def collect_commits(ctx: ReleaseContext, cwd: Path) -> list[Commit]:
-    fmt = "%s%x1f%h%x1f%an"
-    range_ref = git_log_range(ctx)
-    output = run_git(["log", "--no-merges", f"--pretty=format:{fmt}", 
range_ref], cwd)
-    if not output:
-        output = run_git(["log", f"--pretty=format:{fmt}", range_ref], cwd)
-    commits = []
-    for line in output.splitlines():
-        parts = line.split("\x1f")
-        if len(parts) == 3:
-            commits.append(Commit(subject=parts[0], sha=parts[1], 
author=parts[2]))
-    return [commit for commit in commits if not 
is_release_marker(commit.subject)]
-
-
-def is_release_marker(subject: str) -> bool:
-    normalized = subject.strip().lower()
-    return bool(
-        re.fullmatch(r"release[-: ]?v?\d+\.\d+\.\d+", normalized)
-        or re.fullmatch(r"version v?\d+\.\d+\.\d+.*", normalized)
-        or normalized in {"fix version", "bump version", "version bump"}
-    )
-
-
-def classify(subject: str) -> str:
-    value = subject.lower()
-    if any(token in value for token in ["cve", "security", "auth", "jwt", 
"tls", "mtls", "rbac"]):
-        return "security"
-    if any(token in value for token in ["feat", "feature", "add ", "added", 
"new "]):
-        return "features"
-    if any(
-        token in value
-        for token in [
-            "gateway",
-            "xds",
-            "route",
-            "traffic",
-            "meshservice",
-            "multicluster",
-            "east-west",
-            "proxyless",
-            "eds",
-            "lds",
-            "rds",
-        ]
-    ):
-        return "traffic"
-    if any(token in value for token in ["helm", "chart", "install", 
"dubboctl", "operator", "manifest"]):
-        return "installation"
-    if any(token in value for token in ["fix", "bug", "repair", "resolve", 
"correct"]):
-        return "fixes"
-    if any(token in value for token in ["test", "e2e", "coverage"]):
-        return "tests"
-    if any(token in value for token in ["doc", "readme", "website"]):
-        return "docs"
-    return "other"
-
-
-def clean_subject(subject: str) -> str:
-    value = subject.strip()
-    value = 
re.sub(r"^(feat|fix|docs|test|chore|refactor|perf)(\([^)]+\))?:\s*", "", value, 
flags=re.I)
-    return value[:1].upper() + value[1:] if value else subject
-
-
-def format_commit(commit: Commit, repo: str) -> str:
-    subject = clean_subject(commit.subject)
-    match = PR_RE.search(subject)
-    if match:
-        pr = match.group("num")
-        subject = PR_RE.sub("", subject).strip()
-        return f"{subject} ([#{pr}](https://github.com/{repo}/pull/{pr}))"
-    return f"{subject} ({commit.sha})"
-
-
-def grouped_changes(commits: Iterable[Commit], repo: str) -> dict[str, 
list[str]]:
-    groups: dict[str, list[str]] = {key: [] for key, _, _ in CATEGORY_ORDER}
-    seen: set[str] = set()
-    for commit in commits:
-        item = format_commit(commit, repo)
-        if item in seen:
-            continue
-        seen.add(item)
-        groups[classify(commit.subject)].append(item)
-    return {key: values for key, values in groups.items() if values}
-
-
-def render_changes(groups: dict[str, list[str]], language: str) -> str:
-    if not groups:
-        return "- No user-facing changes were detected in the commit range.\n" 
if language == "en" else "- 未检测到面向用户的变更。\n"
-    lines: list[str] = []
-    for key, en_title, zh_title in CATEGORY_ORDER:
-        items = groups.get(key)
-        if not items:
-            continue
-        title = en_title if language == "en" else zh_title
-        lines.append(f"### {title}")
-        lines.append("")
-        for item in items:
-            lines.append(f"- {item}")
-        lines.append("")
-    return "\n".join(lines).rstrip() + "\n"
-
-
-def render_release_body(ctx: ReleaseContext, commits: list[Commit]) -> str:
-    en_kind, _ = release_kind(ctx.version)
-    groups = grouped_changes(commits, ctx.repo)
-    previous = f"Kdubbo {display_version(ctx.previous_tag)}" if 
ctx.previous_tag else "the initial release"
-    return f"""# Announcing Kdubbo {ctx.version}
-
-Kdubbo {ctx.version} {en_kind}.
-
-{english_date(ctx.date)}
-
-This release note describes what is different between {previous} and Kdubbo 
{ctx.version}.
-
-[Before you upgrade](https://kdubbo.github.io/operation/) | 
[Download]({github_release_url(ctx)}) | [Docs](https://kdubbo.github.io/) | 
[Source Changes]({source_changes_url(ctx)})
-
-## Changes
-
-{render_changes(groups, "en")}"""
-
-
-def render_docs_page(ctx: ReleaseContext, commits: list[Commit], language: 
str) -> str:
-    en_kind, zh_kind = release_kind(ctx.version)
-    groups = grouped_changes(commits, ctx.repo)
-    if language == "en":
-        previous = f"Kdubbo {display_version(ctx.previous_tag)}" if 
ctx.previous_tag else "the initial release"
-        return f"""# Announcing Kdubbo {ctx.version}
-
-Kdubbo {ctx.version} {en_kind}.
-
-{english_date(ctx.date)}
-
-This release note describes what is different between {previous} and Kdubbo 
{ctx.version}.
-
-[Before you upgrade](../../../operation/) | 
[Download]({github_release_url(ctx)}) | [Docs](https://kdubbo.github.io/) | 
[Source Changes]({source_changes_url(ctx)})
-
-## Changes
-
-{render_changes(groups, "en")}"""
-
-    previous_zh = f"Kdubbo {display_version(ctx.previous_tag)}" if 
ctx.previous_tag else "初始版本"
-    return f"""# Kdubbo {ctx.version} 发布公告
-
-Kdubbo {ctx.version} {zh_kind}。
-
-发布日期:{ctx.date.isoformat()}
-
-本发布说明描述 {previous_zh} 和 Kdubbo {ctx.version} 之间的差异。
-
-[升级前注意事项](../../../operation/) | [下载]({github_release_url(ctx)}) | 
[文档](https://kdubbo.github.io/) | [源码变更]({source_changes_url(ctx)})
-
-## 变更
-
-{render_changes(groups, "zh")}"""
-
-
-def docs_paths(docs_root: Path, version: str) -> tuple[Path, Path]:
-    base = docs_root / "latest" / "release" / minor_series(version)
-    return base / f"announcing-{version}.md", base / 
f"announcing-{version}.en.md"
-
-
-def render_index_entry(ctx: ReleaseContext, commits: list[Commit]) -> str:
-    groups = grouped_changes(commits, ctx.repo)
-    first_items = []
-    for key, _, zh_title in CATEGORY_ORDER:
-        if key in groups:
-            first_items.append(f"{zh_title}:{groups[key][0]}")
-        if len(first_items) == 3:
-            break
-    summary = "\n".join(f"    - {item}" for item in first_items) or "    - 
查看完整发布说明"
-    page = f"{minor_series(ctx.version)}/announcing-{ctx.version}.md"
-    return f"""    ### [{ctx.version}]({page})
-
-    发布日期:{ctx.date.isoformat()}
-
-{summary}
-
-    [查看完整发布说明]({page})
-    [查看 GitHub Release]({github_release_url(ctx)})
-
-    ---"""
-
-
-def update_docs_index(docs_root: Path, ctx: ReleaseContext, commits: 
list[Commit]) -> None:
-    index = docs_root / "latest" / "release" / "index.md"
-    start = "    <!-- KDUBBO_RELEASE_NOTES:START -->"
-    end = "    <!-- KDUBBO_RELEASE_NOTES:END -->"
-    entry = render_index_entry(ctx, commits)
-    content = index.read_text(encoding="utf-8")
-    if start in content and end in content:
-        before, rest = content.split(start, 1)
-        block, after = rest.split(end, 1)
-        old_entries = [part.strip("\n") for part in block.strip().split("\n\n  
  ---\n\n") if part.strip()]
-        old_entries = [part for part in old_entries if 
f"announcing-{ctx.version}.md" not in part]
-        normalized = [entry.rstrip()]
-        for part in old_entries:
-            normalized.append(part.rstrip() + "\n\n    ---")
-        new_block = "\n\n" + "\n\n".join(normalized).rstrip() + "\n\n"
-        index.write_text(before + start + new_block + end + after, 
encoding="utf-8")
-        return
-
-    anchor = '=== "发布公告"'
-    if anchor not in content:
-        raise ValueError(f"{index} does not contain the release announcement 
tab")
-    replacement = f'{anchor}\n\n{start}\n\n{entry}\n\n{end}\n'
-    index.write_text(content.replace(anchor, replacement, 1), encoding="utf-8")
-
-
-def write_docs(docs_root: Path, ctx: ReleaseContext, commits: list[Commit]) -> 
None:
-    zh_path, en_path = docs_paths(docs_root, ctx.version)
-    zh_path.parent.mkdir(parents=True, exist_ok=True)
-    zh_path.write_text(render_docs_page(ctx, commits, "zh"), encoding="utf-8")
-    en_path.write_text(render_docs_page(ctx, commits, "en"), encoding="utf-8")
-    update_docs_index(docs_root, ctx, commits)
-
-
-def build_context(args: argparse.Namespace, cwd: Path) -> ReleaseContext:
-    tag = strip_ref(args.tag or os.environ.get("GITHUB_REF_NAME", ""))
-    if not tag:
-        tag = strip_ref(run_git(["describe", "--tags", "--abbrev=0"], cwd))
-    version = display_version(tag)
-    parse_version(version)
-    previous_tag = args.previous_tag if args.previous_tag is not None else 
previous_tag_for(tag, cwd)
-    date_value = dt.date.fromisoformat(args.date) if args.date else 
dt.date.today()
-    repo = args.repo or os.environ.get("GITHUB_REPOSITORY", 
"apache/dubbo-kubernetes")
-    return ReleaseContext(tag=tag, version=version, previous_tag=previous_tag, 
repo=repo, date=date_value)
-
-
-def parse_args() -> argparse.Namespace:
-    parser = argparse.ArgumentParser(description="Generate Kdubbo release 
notes and kdubbo.github.io pages.")
-    parser.add_argument("--tag", help="Release tag, for example 0.4.0 or 
v0.4.0.")
-    parser.add_argument("--previous-tag", help="Previous release tag. 
Auto-detected from git tags when omitted.")
-    parser.add_argument("--repo", help="GitHub repository, for example 
apache/dubbo-kubernetes.")
-    parser.add_argument("--date", help="Release date in YYYY-MM-DD format. 
Defaults to today.")
-    parser.add_argument("--release-body", type=Path, help="Write the GitHub 
Release body markdown to this file.")
-    parser.add_argument("--docs-root", type=Path, help="Path to 
kdubbo.github.io/docs to update.")
-    parser.add_argument("--print", action="store_true", help="Print the GitHub 
Release body to stdout.")
-    return parser.parse_args()
-
-
-def main() -> int:
-    args = parse_args()
-    cwd = Path.cwd()
-    ctx = build_context(args, cwd)
-    commits = collect_commits(ctx, cwd)
-    body = render_release_body(ctx, commits)
-    if args.release_body:
-        args.release_body.parent.mkdir(parents=True, exist_ok=True)
-        args.release_body.write_text(body, encoding="utf-8")
-    if args.docs_root:
-        write_docs(args.docs_root, ctx, commits)
-    if args.print:
-        print(body)
-    return 0
-
-
-if __name__ == "__main__":
-    raise SystemExit(main())
diff --git a/tools/release_notes/generate_release_notes_test.py 
b/tools/release_notes/generate_release_notes_test.py
deleted file mode 100644
index 8e5a2d7b..00000000
--- a/tools/release_notes/generate_release_notes_test.py
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/usr/bin/env python3
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from __future__ import annotations
-
-import datetime as dt
-import importlib.util
-from pathlib import Path
-import sys
-import tempfile
-import unittest
-
-
-MODULE_PATH = Path(__file__).with_name("generate_release_notes.py")
-SPEC = importlib.util.spec_from_file_location("generate_release_notes", 
MODULE_PATH)
-assert SPEC and SPEC.loader
-release_notes = importlib.util.module_from_spec(SPEC)
-sys.modules[SPEC.name] = release_notes
-SPEC.loader.exec_module(release_notes)
-
-
-class ReleaseNotesTest(unittest.TestCase):
-    def context(self):
-        return release_notes.ReleaseContext(
-            tag="0.4.1",
-            version="0.4.1",
-            previous_tag="0.4.0",
-            repo="apache/dubbo-kubernetes",
-            date=dt.date(2026, 5, 18),
-        )
-
-    def commits(self):
-        return [
-            release_notes.Commit("feat: Add multicluster east-west gateway 
(#910)", "abc1234", "dev"),
-            release_notes.Commit("fix: Repair remote webhook CA bundle 
(#911)", "def5678", "dev"),
-            release_notes.Commit("docs: Update install guide", "abcd999", 
"dev"),
-        ]
-
-    def test_release_body_uses_istio_style_sections(self):
-        body = release_notes.render_release_body(self.context(), 
self.commits())
-
-        self.assertIn("# Announcing Kdubbo 0.4.1", body)
-        self.assertIn("Kdubbo 0.4.1 patch release.", body)
-        self.assertIn("May 18, 2026", body)
-        self.assertIn("[Before you upgrade]", body)
-        self.assertIn("[Source 
Changes](https://github.com/apache/dubbo-kubernetes/compare/0.4.0...0.4.1)", 
body)
-        self.assertIn("### Features", body)
-        
self.assertIn("[#910](https://github.com/apache/dubbo-kubernetes/pull/910)", 
body)
-
-    def test_writes_bilingual_docs_and_index_entry(self):
-        with tempfile.TemporaryDirectory() as temp:
-            docs = Path(temp) / "docs"
-            release_dir = docs / "latest" / "release"
-            release_dir.mkdir(parents=True)
-            (release_dir / "index.md").write_text('# 公告栏\n\n=== "发布公告"\n\n    
## 0.3.x\n', encoding="utf-8")
-
-            release_notes.write_docs(docs, self.context(), self.commits())
-
-            zh = release_dir / "0.4.x" / "announcing-0.4.1.md"
-            en = release_dir / "0.4.x" / "announcing-0.4.1.en.md"
-            index = release_dir / "index.md"
-
-            self.assertTrue(zh.exists())
-            self.assertTrue(en.exists())
-            self.assertIn("# Kdubbo 0.4.1 发布公告", 
zh.read_text(encoding="utf-8"))
-            self.assertIn("# Announcing Kdubbo 0.4.1", 
en.read_text(encoding="utf-8"))
-            self.assertIn("KDUBBO_RELEASE_NOTES:START", 
index.read_text(encoding="utf-8"))
-            self.assertIn("[0.4.1](0.4.x/announcing-0.4.1.md)", 
index.read_text(encoding="utf-8"))
-
-
-if __name__ == "__main__":
-    unittest.main()


Reply via email to