You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/09/17 08:10:35 UTC
(skywalking-ai-sessionizer) branch binary-distribution created (now 5e10afb)
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a change to branch binary-distribution
in repository https://gitbox.apache.org/repos/asf/skywalking-ai-sessionizer.git
at 5e10afb Keep every release installable with Homebrew, and add the binary-distribution skill
This branch includes the following new commits:
new 5e10afb Keep every release installable with Homebrew, and add the binary-distribution skill
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(skywalking-ai-sessionizer) 01/01: Keep every release installable with Homebrew, and add the binary-distribution skill
Posted by wu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch binary-distribution
in repository https://gitbox.apache.org/repos/asf/skywalking-ai-sessionizer.git
commit 5e10afbd0465d2a271e15b772d49aaa59cb012d7
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Thu Sep 17 16:10:29 2026 +0800
Keep every release installable with Homebrew, and add the binary-distribution skill
A formula names one version, so once asz.rb moves to the next release the
previous one could no longer be installed. Each release now also gets
asz@VERSION and asz-claude-code@VERSION, which Formula/ keeps. They are
keg-only, as Homebrew wants for an alternate version, so installing one
beside the current formula does not replace its binary on PATH.
Homebrew relates asz@VERSION to asz only when VERSION is digits and dots.
With the CI version 0.0.0-ci it did not, linked asz@0.0.0-ci over asz, and
the install failed. Every Apache release is digits and dots, so the
generator writes versioned formulae only for such a version, the check
refuses any other, and CI builds its packages as 0.0.0. The check also
removes a formula whose install stopped part way, which it left behind.
tools/homebrew-formula.sh [--from DIR] [--check] VERSION... writes the
formulae of released versions into Formula/. It downloads each version's
packages from downloads.apache.org, or archive.apache.org for an older one,
holds them to their sha512, runs tools/homebrew-check.sh first with --check,
and moves asz.rb and asz-claude-code.rb only forward, so adding an older
version never moves them back. Tested with two local versions and a re-add
of the older one, and against the real sites with 0.3.0, whose old package
layout it refuses, and 0.2.9, which was never released.
.claude/skills/binary-distribution runs it for one version or several,
checks each version is released first, and opens the pull request.
---
.claude/skills/binary-distribution/SKILL.md | 94 +++++++++++++++++++++
.github/workflows/ci.yaml | 4 +-
docs/en/changes/changes.md | 5 ++
docs/en/guides/how-to-release.md | 26 +++---
docs/en/setup/install.md | 4 +
tools/homebrew-check.sh | 22 ++++-
tools/homebrew-formula.sh | 124 ++++++++++++++++++++++++++++
tools/install-manifests.sh | 27 ++++++
8 files changed, 289 insertions(+), 17 deletions(-)
diff --git a/.claude/skills/binary-distribution/SKILL.md b/.claude/skills/binary-distribution/SKILL.md
new file mode 100644
index 0000000..7188fad
--- /dev/null
+++ b/.claude/skills/binary-distribution/SKILL.md
@@ -0,0 +1,94 @@
+---
+name: binary-distribution
+description: Put released versions of Apache SkyWalking AI Sessionizer into Homebrew by updating Formula/ on main in apache/skywalking-ai-sessionizer, the tap. Writes asz@VERSION and asz-claude-code@VERSION for each version, moves asz and asz-claude-code to the newest, checks the formulae with Homebrew, and opens the pull request. Use after a version is published, or to add older released versions.
+user-invocable: true
+---
+
+# Binary distribution: Homebrew
+
+`Formula/` on main is the Homebrew tap. It holds, for every released version,
+`asz@VERSION.rb` and `asz-claude-code@VERSION.rb`, and `asz.rb` and
+`asz-claude-code.rb` for the newest version. Users install with:
+
+```sh
+brew tap apache/skywalking-ai-sessionizer https://github.com/apache/skywalking-ai-sessionizer
+brew install apache/skywalking-ai-sessionizer/asz
+brew install apache/skywalking-ai-sessionizer/asz-claude-code@0.4.0
+```
+
+The user names one version or several. A version must be released: voted,
+moved to the download site, and its GitHub release promoted, because the
+formulae download the packages from that GitHub release.
+
+## 1. Check the versions are released
+
+Each version must answer 200 on one of the two Apache sites, and its GitHub
+release must carry the packages. A local proxy can break HTTPS to Apache hosts,
+so these commands go around it:
+
+```sh
+for v in <versions>; do
+ for u in "https://downloads.apache.org/skywalking/ai-sessionizer/$v/" "https://archive.apache.org/dist/skywalking/ai-sessionizer/$v/"; do
+ curl --noproxy '*' -s -o /dev/null -w "$v $u %{http_code}\n" "$u"
+ done
+ gh release view "v$v" --repo apache/skywalking-ai-sessionizer --json isPrerelease,assets \
+ --jq '"v'"$v"' prerelease=\(.isPrerelease) assets=\(.assets|length)"'
+done
+```
+
+Stop and tell the user about any version that is not released, or whose release
+is still a prerelease.
+
+## 2. Branch from main
+
+Work in a worktree so another session's changes in the main clone are left
+alone:
+
+```sh
+git fetch origin
+git worktree add -b brew-<versions> <scratch dir> origin/main
+cd <scratch dir>
+```
+
+## 3. Write and check the formulae, on macOS
+
+```sh
+env -u http_proxy -u https_proxy -u all_proxy tools/homebrew-formula.sh --check <versions>
+```
+
+For each version it downloads the six packages and their `.sha512` from the
+Apache sites, holds each package to its `.sha512`, and runs the formulae
+through `brew style`, `brew audit --strict`, `brew install` and `brew test`,
+removing what it installed. Only then does it write `Formula/asz@VERSION.rb`
+and `Formula/asz-claude-code@VERSION.rb`, and move `Formula/asz.rb` and
+`Formula/asz-claude-code.rb` to the newest version. Adding an older version
+never moves them back. With `--from DIR` it reads `DIR/VERSION/` instead, such
+as the release manager's `dist/`.
+
+If `asz` or `asz-claude-code` is already installed on this machine, the check
+refuses to run rather than replace it. Say so and stop; do not uninstall the
+user's own install.
+
+## 4. Look at the change
+
+Read what it printed, and `git status --short`. Only files under `Formula/` may
+change.
+
+## 5. Commit and open the pull request
+
+```sh
+git add Formula
+git commit -m "Homebrew formulae for <versions>"
+git push -u origin brew-<versions>
+gh pr create --repo apache/skywalking-ai-sessionizer --base main --title "Homebrew formulae for <versions>"
+```
+
+The commit message and the pull request carry no AI attribution: no
+Co-Authored-By line and no "Generated with" line. Say in the pull request which
+versions were added, which version `asz` and `asz-claude-code` now install, and
+that the check passed.
+
+## 6. After the merge
+
+Anyone who tapped runs `brew update`, then `brew upgrade asz asz-claude-code`.
+Tell the user the pull request link, and remove the worktree once it is merged.
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index e6627de..da69a1c 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -411,8 +411,8 @@ jobs:
echo "::notice::this tag has no tools/homebrew-check.sh, so the formulae are not run"
exit 0
fi
- make binaries checksums VERSION=0.0.0-ci
- tools/homebrew-check.sh 0.0.0-ci dist
+ make binaries checksums VERSION=0.0.0
+ tools/homebrew-check.sh 0.0.0 dist
lint:
name: Lint
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index 744f5b8..8115545 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -100,6 +100,11 @@
then `brew install apache/skywalking-ai-sessionizer/asz` and `.../asz-claude-code`.
`tools/homebrew-check.sh` runs both formulae through `brew style`, `brew audit --strict`,
`brew install` and `brew test`, and CI's `homebrew` job runs it on macOS on every change.
+- **Every release stays installable with Homebrew.** Each release adds `asz@VERSION` and
+ `asz-claude-code@VERSION`, keg-only, beside the current formulae. `tools/homebrew-formula.sh`
+ writes them into `Formula/` from the released packages, checks them with Homebrew first, and
+ moves `asz` and `asz-claude-code` only forward. The `binary-distribution` skill runs it and opens
+ the pull request.
- **Two install scripts, one for each install.** asz, the collector, and the Claude Code plugin are
installed apart. `install/asz.sh` and `install/asz.ps1` install `asz`, and
[Install](../setup/install.md#install-script) runs them from the version's tag in one command. The
diff --git a/docs/en/guides/how-to-release.md b/docs/en/guides/how-to-release.md
index 849db6f..667f4b9 100644
--- a/docs/en/guides/how-to-release.md
+++ b/docs/en/guides/how-to-release.md
@@ -929,22 +929,24 @@ Each installs `LICENSE`, `NOTICE` and `licenses/`, and its test runs its binary.
Homebrew/homebrew-core takes a formula only when it builds from source, and these install a binary
built for each platform.
-The tap is this repository. Homebrew looks for a tap's formulae in `Formula/` first, so after
-Publish, open a pull request to main that replaces `Formula/asz.rb` and `Formula/asz-claude-code.rb`
-with the two files from `dist/$VERSION/install/homebrew/`. The formulae name the sha256 of the voted
-packages, so they cannot be committed before the vote, and the tag of a version never holds its own
-formulae. `.gitattributes` keeps `Formula/` out of the source package, which would otherwise carry
-the formulae of the version before it. Users add the tap and install by the full name, as
-[Install](../setup/install.md#homebrew-on-macos-and-linux) shows.
-
-Before the pull request, run the formulae through Homebrew on the voted packages. It writes them into
-a local tap, runs `brew style` and `brew audit --strict`, installs and tests both, and removes
-everything it installed:
+The tap is this repository: `Formula/` on main. It keeps `asz@VERSION.rb` and
+`asz-claude-code@VERSION.rb` for every release, keg-only so they do not clash, and `asz.rb` and
+`asz-claude-code.rb` for the newest. The formulae name the sha256 of the voted packages, so they
+cannot be committed before the vote, and the tag of a version never holds its own formulae.
+`.gitattributes` keeps `Formula/` out of the source package.
+
+After Publish, on macOS, on a branch from main:
```sh
-tools/homebrew-check.sh $VERSION dist/$VERSION
+tools/homebrew-formula.sh --from dist --check $VERSION
```
+It runs each version's formulae through `brew style`, `brew audit --strict`, `brew install` and
+`brew test`, and only then writes them into `Formula/`, moving `asz.rb` and `asz-claude-code.rb` to
+the newest version. Without `--from` it downloads released packages from the Apache sites, which is
+how older versions are added. Open a pull request with the change to `Formula/`. The
+`binary-distribution` skill in `.claude/skills/` does all of this with Claude Code.
+
CI's `homebrew` job runs the same script on every change, on packages it builds with a version of
its own, so a change that breaks the formulae fails before a release.
diff --git a/docs/en/setup/install.md b/docs/en/setup/install.md
index 57acd89..4a00834 100644
--- a/docs/en/setup/install.md
+++ b/docs/en/setup/install.md
@@ -37,6 +37,10 @@ brew install apache/skywalking-ai-sessionizer/asz-claude-code
plugin into Claude Code, run the two commands that `brew info asz-claude-code` prints. Upgrade with
`brew upgrade asz asz-claude-code`.
+For an exact version, install `apache/skywalking-ai-sessionizer/asz@<version>` and
+`apache/skywalking-ai-sessionizer/asz-claude-code@<version>`. Homebrew keeps them off your `PATH`;
+`brew info` shows where they are.
+
## Binary package
The [downloads page](https://skywalking.apache.org/downloads/) links one package per platform:
diff --git a/tools/homebrew-check.sh b/tools/homebrew-check.sh
index fc3fbd7..64215bb 100755
--- a/tools/homebrew-check.sh
+++ b/tools/homebrew-check.sh
@@ -40,6 +40,9 @@ step() { printf '\n== %s\n' "$*"; }
[ "$#" -eq 2 ] || { echo "usage: tools/homebrew-check.sh VERSION PKG_DIR" >&2; exit 2; }
version=$1
+# Homebrew takes asz@VERSION as a version of asz only when VERSION is digits
+# and dots. With any other VERSION it links the versioned formula over asz.
+printf '%s' "$version" | grep -Eq '^[0-9]+(\.[0-9]+)*$' || fail "$version is not digits and dots, which a Homebrew versioned formula needs"
[ -d "$2" ] || fail "$2 is not a directory"
pkg_dir=$(cd "$2" && pwd)
tree=$(cd "$(dirname "$0")/.." && pwd)
@@ -48,11 +51,13 @@ command -v perl >/dev/null 2>&1 || fail "perl is not on PATH"
export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ANALYTICS=1 HOMEBREW_NO_INSTALL_CLEANUP=1 HOMEBREW_NO_ENV_HINTS=1
tap=aszcheck/formulae
-formulae="asz asz-claude-code"
+formulae="asz asz-claude-code asz@$version asz-claude-code@$version"
work=$(mktemp -d)
installed=""
cleanup() {
- for f in $installed; do brew uninstall --formula "$f" >/dev/null 2>&1 || true; done
+ # A formula whose install stopped part way is installed but not recorded,
+ # so every one this check could have installed is removed.
+ for f in $formulae $installed; do brew uninstall --formula "$tap/$f" >/dev/null 2>&1 || true; done
brew untap "$tap" >/dev/null 2>&1 || true
rm -rf "$work"
}
@@ -98,6 +103,17 @@ brew info --formula "$tap/asz-claude-code" | grep -qF "claude plugin marketplace
[ ! -e "$prefix/bin/asz-claude-plugin" ] || [ -e "$(brew --prefix asz-claude-code)/bin/asz-claude-plugin" ] ||
fail "asz-claude-plugin does not come from asz-claude-code"
[ ! -e "$(brew --prefix asz)/bin/asz-claude-plugin" ] || fail "asz installs asz-claude-plugin too, which is the plugin's own formula"
+# A versioned formula is keg-only: its binary is in its own prefix, and the
+# one on PATH stays the current formula's.
+for f in asz asz-claude-code; do
+ b=asz
+ [ "$f" = asz ] || b=asz-claude-plugin
+ "$(brew --prefix "$f@$version")/bin/$b" version | grep -qF "$version" || fail "$f@$version has no $b that reports $version"
+ case "$(readlink "$prefix/bin/$b")" in
+ *"/Cellar/$f/"*) ;;
+ *) fail "$b on Homebrew's PATH is not the one $f installed: $(readlink "$prefix/bin/$b")" ;;
+ esac
+done
step "Done"
-echo "brew install asz and brew install asz-claude-code work for $version"
+echo "brew install asz, asz-claude-code, asz@$version and asz-claude-code@$version work"
diff --git a/tools/homebrew-formula.sh b/tools/homebrew-formula.sh
new file mode 100755
index 0000000..1a45538
--- /dev/null
+++ b/tools/homebrew-formula.sh
@@ -0,0 +1,124 @@
+#!/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.
+#
+
+# Writes the Homebrew formulae of released versions into Formula/, the tap
+# this repository is.
+#
+# tools/homebrew-formula.sh [--from DIR] [--check] VERSION...
+#
+# For each VERSION it takes the six binary packages and their .sha512 from
+# DIR/VERSION, or else downloads them from downloads.apache.org, or from
+# archive.apache.org for a version the download site no longer holds.
+# tools/install-manifests.sh holds each package to its .sha512 and writes the
+# formulae.
+#
+# Every VERSION gets Formula/asz@VERSION.rb and Formula/asz-claude-code@VERSION.rb.
+# Formula/asz.rb and Formula/asz-claude-code.rb follow the newest version among
+# those given and the one Formula/asz.rb names already, so adding an older
+# version never moves them back. The script changes Formula/ only, and leaves
+# the commit and the pull request to the person who runs it.
+#
+# With --check, each version's formulae go through tools/homebrew-check.sh,
+# brew style, brew audit, brew install and brew test, before any of them is
+# written into Formula/. It needs Homebrew, and runs on macOS.
+
+set -euo pipefail
+
+me=homebrew-formula
+fail() { printf '%s: %s\n' "$me" "$*" >&2; exit 1; }
+say() { printf '%s: %s\n' "$me" "$*"; }
+
+from=""
+check=false
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --from)
+ [ -n "${2:-}" ] || fail "--from needs a directory"
+ from=$(cd "$2" && pwd) || fail "$2 is not a directory"
+ shift 2 ;;
+ --check) check=true; shift ;;
+ --*) fail "unknown option $1" ;;
+ *) break ;;
+ esac
+done
+[ "$#" -ge 1 ] || { echo "usage: tools/homebrew-formula.sh [--from DIR] [--check] VERSION..." >&2; exit 2; }
+for v in "$@"; do
+ # Homebrew takes asz@VERSION as a version of asz only when VERSION is
+ # digits and dots, which every Apache release is.
+ printf '%s' "$v" | grep -Eq '^[0-9]+(\.[0-9]+)*$' || fail "$v is not a released version: digits and dots"
+done
+
+tree=$(cd "$(dirname "$0")/.." && pwd)
+formula=$tree/Formula
+work=$(mktemp -d)
+trap 'rm -rf "$work"' EXIT
+
+# The version the current formulae install, read from the release path in
+# their download URL.
+current=""
+if [ -f "$formula/asz.rb" ]; then
+ current=$(grep -Eo '/releases/download/v[0-9.]+/' "$formula/asz.rb" | head -1 | sed -E 's#/releases/download/v([0-9.]+)/#\1#')
+fi
+
+packages() {
+ local v=$1 dir=$2 base=apache-skywalking-ai-sessionizer-$1-bin
+ mkdir -p "$dir"
+ for p in darwin-arm64.tgz darwin-amd64.tgz linux-arm64.tgz linux-amd64.tgz windows-amd64.zip windows-arm64.zip; do
+ for f in "$base-$p" "$base-$p.sha512"; do
+ local site=https://downloads.apache.org/skywalking/ai-sessionizer/$v/$f
+ local archive=https://archive.apache.org/dist/skywalking/ai-sessionizer/$v/$f
+ local code
+ code=$(curl -sSL -o "$dir/$f" -w '%{http_code}' "$site") || fail "cannot reach downloads.apache.org for $f"
+ case "$code" in
+ 200) ;;
+ 404) curl -fsSL -o "$dir/$f" "$archive" || fail "neither downloads.apache.org nor archive.apache.org has $f. Is $v released?" ;;
+ *) fail "downloads.apache.org answered $code for $f" ;;
+ esac
+ done
+ done
+}
+
+newest=$current
+for v in "$@"; do
+ if [ -n "$from" ]; then
+ dir=$from/$v
+ [ -d "$dir" ] || fail "$dir does not exist"
+ else
+ say "downloading the packages of $v"
+ dir=$work/packages-$v
+ packages "$v" "$dir"
+ fi
+ bash "$tree/tools/install-manifests.sh" "$v" "$dir" "$work/manifests-$v" >/dev/null
+ if [ "$check" = true ]; then
+ say "checking the formulae of $v with Homebrew"
+ bash "$tree/tools/homebrew-check.sh" "$v" "$dir" || fail "the formulae of $v did not pass tools/homebrew-check.sh, so none of $v was written"
+ fi
+ mkdir -p "$formula"
+ cp "$work/manifests-$v/homebrew/asz@$v.rb" "$work/manifests-$v/homebrew/asz-claude-code@$v.rb" "$formula/"
+ say "wrote Formula/asz@$v.rb and Formula/asz-claude-code@$v.rb"
+ newest=$(printf '%s\n%s\n' "$newest" "$v" | sed '/^$/d' | sort -V | tail -1)
+done
+
+if [ "$newest" != "$current" ]; then
+ cp "$work/manifests-$newest/homebrew/asz.rb" "$work/manifests-$newest/homebrew/asz-claude-code.rb" "$formula/"
+ say "Formula/asz.rb and Formula/asz-claude-code.rb now install $newest${current:+, not $current}"
+else
+ say "Formula/asz.rb and Formula/asz-claude-code.rb stay at $current, the newest version"
+fi
diff --git a/tools/install-manifests.sh b/tools/install-manifests.sh
index bf37c18..ffd8dd8 100755
--- a/tools/install-manifests.sh
+++ b/tools/install-manifests.sh
@@ -32,6 +32,8 @@
#
# homebrew/asz.rb a formula that installs asz from the macOS and Linux packages
# homebrew/asz-claude-code.rb a formula that installs the Claude Code plugin's binary
+# homebrew/asz@VERSION.rb, asz-claude-code@VERSION.rb
+# the same, keg-only, kept for every release
# scoop/skywalking-ai-sessionizer.json a Scoop manifest for the Windows packages
# winget/manifests/a/Apache/SkyWalkingAISessionizer/VERSION/
# the three winget manifests
@@ -378,6 +380,30 @@ end
EOF
} | fill > "$out_dir/homebrew/asz-claude-code.rb"
+# A versioned formula of each, asz@VERSION and asz-claude-code@VERSION, which
+# Formula/ keeps for every release, so an exact older version stays
+# installable after the current formulae move on. It is keg-only, as
+# Homebrew wants for an alternate version, so it does not clash with the
+# current formula's binary. The class name is Homebrew's for the file name:
+# asz@0.4.0 is AszAT040.
+homebrew_class() {
+ printf '%s' "$1" | perl -pe '$_ = ucfirst; s/[-_.\s]([a-zA-Z0-9])/\U$1/g; tr/+/x/; s/(.)@(\d)/$1AT$2/'
+}
+# Homebrew takes asz@VERSION as a version of asz only when VERSION is digits
+# and dots, which every Apache release is. A candidate's version with a
+# suffix gets no versioned formula.
+homebrew_versioned=asz
+printf '%s' "$version" | grep -Eq '^[0-9]+(\.[0-9]+)*$' || homebrew_versioned=
+for f in ${homebrew_versioned:+asz asz-claude-code}; do
+ current=$(homebrew_class "$f")
+ versioned=$(homebrew_class "$f@$version")
+ CURRENT=$current VERSIONED=$versioned perl -pe \
+ 's/^class \Q$ENV{CURRENT}\E < Formula$/class $ENV{VERSIONED} < Formula/; s/^( license "Apache-2\.0"\n)/$1\n keg_only :versioned_formula\n/' \
+ "$out_dir/homebrew/$f.rb" > "$out_dir/homebrew/$f@$version.rb"
+ grep -q "^class $versioned < Formula$" "$out_dir/homebrew/$f@$version.rb" && grep -q '^ keg_only :versioned_formula$' "$out_dir/homebrew/$f@$version.rb" ||
+ fail "could not write $f@$version.rb from $f.rb"
+done
+
# --------------------------------------------------------------------- Scoop
# The notes name the plugin's tag as VERSION for the reader to fill in. Scoop
# replaces only $dir, $original_dir and $persist_dir in notes, and
@@ -699,6 +725,7 @@ if grep -rn '@[A-Z0-9_]*@' "$out_dir" >&2; then fail "a placeholder above was no
cat <<DONE
wrote the manifests for $version into $out_dir:
homebrew/asz.rb, asz-claude-code.rb darwin and linux, arm64 and amd64, sha256
+ homebrew/asz@$version.rb, asz-claude-code@$version.rb
scoop/skywalking-ai-sessionizer.json windows-amd64 and windows-arm64, sha512
$winget_dir/ schema $winget_schema, sha256
README.md