- Add cliff.toml for git-cliff changelog generation (one-line entries, no commit body dumps, improve/refine prefixes mapped) - Add @changesets/cli config and README in .changeset/ - Add release.sh script with per-package version bumps from changesets, changeset-driven per-crate changelog updates, and --all/--dry-run flags - Switch all crates from workspace version to independent version = "0.1.0" - Generate clean root CHANGELOG.md and per-crate CHANGELOGs with [0.1.0] - Retag v1.0.0 → v0.1.0 to match actual crate versions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
356 lines
11 KiB
Bash
Executable file
356 lines
11 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# DreamStack Release Script
|
|
#
|
|
# Reads .changeset/*.md files (YAML frontmatter with package bump types),
|
|
# bumps per-package versions, regenerates changelogs, commits, and tags.
|
|
#
|
|
# Usage:
|
|
# ./scripts/release.sh # apply changesets
|
|
# ./scripts/release.sh --dry-run # preview without committing
|
|
# ./scripts/release.sh --all <bump> # bump all packages (patch|minor|major)
|
|
#
|
|
set -euo pipefail
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
WORKSPACE_TOML="$ROOT_DIR/Cargo.toml"
|
|
CHANGELOG="$ROOT_DIR/CHANGELOG.md"
|
|
CHANGESET_DIR="$ROOT_DIR/.changeset"
|
|
|
|
DRY_RUN=false
|
|
BUMP_ALL=""
|
|
|
|
# ── Package registry ─────────────────────────────────────────────────
|
|
# name:path pairs for all workspace crates
|
|
declare -A PKG_PATH=(
|
|
[ds-parser]="compiler/ds-parser"
|
|
[ds-analyzer]="compiler/ds-analyzer"
|
|
[ds-codegen]="compiler/ds-codegen"
|
|
[ds-layout]="compiler/ds-layout"
|
|
[ds-types]="compiler/ds-types"
|
|
[ds-incremental]="compiler/ds-incremental"
|
|
[ds-cli]="compiler/ds-cli"
|
|
[ds-physics]="engine/ds-physics"
|
|
[ds-stream]="engine/ds-stream"
|
|
[ds-stream-wasm]="engine/ds-stream-wasm"
|
|
)
|
|
|
|
# ── Parse flags ──────────────────────────────────────────────────────
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
--all)
|
|
shift
|
|
if [[ -z "${1:-}" ]]; then
|
|
echo -e "${RED}Error:${NC} --all requires a bump type (patch|minor|major)"
|
|
exit 1
|
|
fi
|
|
BUMP_ALL="$1"; shift
|
|
;;
|
|
*) echo -e "${RED}Error:${NC} Unknown argument: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
get_pkg_version() {
|
|
local cargo_toml="$1/Cargo.toml"
|
|
grep '^version' "$cargo_toml" | head -1 | sed 's/.*"\(.*\)".*/\1/'
|
|
}
|
|
|
|
bump_version() {
|
|
local current="$1" bump="$2"
|
|
local major minor patch
|
|
IFS='.' read -r major minor patch <<< "$current"
|
|
case "$bump" in
|
|
major) echo "$((major + 1)).0.0" ;;
|
|
minor) echo "$major.$((minor + 1)).0" ;;
|
|
patch) echo "$major.$minor.$((patch + 1))" ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
set_pkg_version() {
|
|
local cargo_toml="$1/Cargo.toml"
|
|
local old_ver="$2" new_ver="$3"
|
|
sed -i "0,/^version = \"$old_ver\"/s//version = \"$new_ver\"/" "$cargo_toml"
|
|
}
|
|
|
|
# ── Parse changesets ─────────────────────────────────────────────────
|
|
# Reads YAML frontmatter from .changeset/*.md files
|
|
# Format:
|
|
# ---
|
|
# "ds-parser": minor
|
|
# "ds-codegen": patch
|
|
# ---
|
|
# Description of the change.
|
|
|
|
declare -A BUMPS # package -> highest bump (major > minor > patch)
|
|
declare -a DESCRIPTIONS # changeset descriptions
|
|
|
|
BUMP_RANK_patch=1
|
|
BUMP_RANK_minor=2
|
|
BUMP_RANK_major=3
|
|
|
|
higher_bump() {
|
|
local current="$1" candidate="$2"
|
|
if [[ -z "$current" ]]; then echo "$candidate"; return; fi
|
|
local cur_rank="BUMP_RANK_$current"
|
|
local cand_rank="BUMP_RANK_$candidate"
|
|
if [[ "${!cand_rank}" -gt "${!cur_rank}" ]]; then
|
|
echo "$candidate"
|
|
else
|
|
echo "$current"
|
|
fi
|
|
}
|
|
|
|
parse_changesets() {
|
|
shopt -s nullglob
|
|
local files=("$CHANGESET_DIR"/*.md)
|
|
shopt -u nullglob
|
|
|
|
for f in "${files[@]}"; do
|
|
[[ "$(basename "$f")" == "README.md" ]] && continue
|
|
|
|
local in_frontmatter=false
|
|
local description=""
|
|
|
|
while IFS= read -r line; do
|
|
if [[ "$line" == "---" ]]; then
|
|
if [[ "$in_frontmatter" == false ]]; then
|
|
in_frontmatter=true
|
|
continue
|
|
else
|
|
in_frontmatter=false
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
if [[ "$in_frontmatter" == true ]]; then
|
|
# Parse: "pkg-name": bump_type
|
|
local pkg bump
|
|
pkg=$(echo "$line" | sed -n 's/^"\([^"]*\)":.*/\1/p')
|
|
bump=$(echo "$line" | sed -n 's/^"[^"]*": *\(.*\)/\1/p' | tr -d ' ')
|
|
if [[ -n "$pkg" && -n "$bump" ]]; then
|
|
BUMPS[$pkg]=$(higher_bump "${BUMPS[$pkg]:-}" "$bump")
|
|
fi
|
|
else
|
|
if [[ -n "$line" ]]; then
|
|
description+="$line"$'\n'
|
|
fi
|
|
fi
|
|
done < "$f"
|
|
|
|
if [[ -n "$description" ]]; then
|
|
DESCRIPTIONS+=("$description")
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ── Collect bumps ────────────────────────────────────────────────────
|
|
|
|
DESCRIPTIONS=()
|
|
|
|
if [[ -n "$BUMP_ALL" ]]; then
|
|
for pkg in "${!PKG_PATH[@]}"; do
|
|
BUMPS[$pkg]="$BUMP_ALL"
|
|
done
|
|
echo -e "${CYAN}Bumping all packages:${NC} $BUMP_ALL"
|
|
else
|
|
parse_changesets
|
|
fi
|
|
|
|
if [[ ${#BUMPS[@]} -eq 0 ]]; then
|
|
echo -e "${YELLOW}No changesets found.${NC} Nothing to release."
|
|
echo "Add changesets in .changeset/ or use --all <bump>"
|
|
exit 0
|
|
fi
|
|
|
|
# ── Preview ──────────────────────────────────────────────────────────
|
|
|
|
echo -e "${CYAN}DreamStack Release${NC}"
|
|
echo ""
|
|
|
|
for pkg in $(echo "${!BUMPS[@]}" | tr ' ' '\n' | sort); do
|
|
local_path="${PKG_PATH[$pkg]:-}"
|
|
if [[ -z "$local_path" ]]; then
|
|
echo -e " ${YELLOW}⚠${NC} Unknown package: $pkg (skipping)"
|
|
continue
|
|
fi
|
|
|
|
current=$(get_pkg_version "$ROOT_DIR/$local_path")
|
|
new=$(bump_version "$current" "${BUMPS[$pkg]}")
|
|
echo -e " ${GREEN}$pkg${NC} $current → $new (${BUMPS[$pkg]})"
|
|
done
|
|
|
|
echo ""
|
|
|
|
if [[ ${#DESCRIPTIONS[@]} -gt 0 ]]; then
|
|
echo -e "${CYAN}Changeset descriptions:${NC}"
|
|
for desc in "${DESCRIPTIONS[@]}"; do
|
|
echo " - $(echo "$desc" | head -1)"
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo -e "${YELLOW}[DRY RUN]${NC} No changes made."
|
|
exit 0
|
|
fi
|
|
|
|
# ── Check prerequisites ─────────────────────────────────────────────
|
|
|
|
if ! command -v git-cliff &>/dev/null; then
|
|
echo -e "${RED}Error:${NC} git-cliff not found. Install with: cargo install git-cliff"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "$(git -C "$ROOT_DIR" status --porcelain)" ]]; then
|
|
echo -e "${RED}Error:${NC} Working directory is not clean. Commit or stash changes first."
|
|
exit 1
|
|
fi
|
|
|
|
# ── Step 1: Bump per-package versions ────────────────────────────────
|
|
|
|
echo -e "${GREEN}→${NC} Bumping package versions..."
|
|
|
|
HIGHEST_BUMP=""
|
|
for pkg in "${!BUMPS[@]}"; do
|
|
local_path="${PKG_PATH[$pkg]:-}"
|
|
[[ -z "$local_path" ]] && continue
|
|
|
|
pkg_dir="$ROOT_DIR/$local_path"
|
|
current=$(get_pkg_version "$pkg_dir")
|
|
new=$(bump_version "$current" "${BUMPS[$pkg]}")
|
|
|
|
if [[ -z "$new" ]]; then
|
|
echo -e " ${RED}✗${NC} $pkg — invalid bump '${BUMPS[$pkg]}'"
|
|
continue
|
|
fi
|
|
|
|
set_pkg_version "$pkg_dir" "$current" "$new"
|
|
echo -e " ${GREEN}✓${NC} $pkg $current → $new"
|
|
|
|
HIGHEST_BUMP=$(higher_bump "$HIGHEST_BUMP" "${BUMPS[$pkg]}")
|
|
done
|
|
|
|
# ── Step 2: Bump workspace version ──────────────────────────────────
|
|
|
|
WORKSPACE_VERSION=$(grep -A2 '^\[workspace\.package\]' "$WORKSPACE_TOML" \
|
|
| grep '^version' | sed 's/version = "\(.*\)"/\1/')
|
|
NEW_WORKSPACE=$(bump_version "$WORKSPACE_VERSION" "$HIGHEST_BUMP")
|
|
|
|
if [[ -n "$NEW_WORKSPACE" ]]; then
|
|
sed -i "s/^version = \"$WORKSPACE_VERSION\"/version = \"$NEW_WORKSPACE\"/" "$WORKSPACE_TOML"
|
|
echo -e " ${GREEN}✓${NC} workspace $WORKSPACE_VERSION → $NEW_WORKSPACE"
|
|
fi
|
|
|
|
VERSION_TAG="v${NEW_WORKSPACE:-$WORKSPACE_VERSION}"
|
|
|
|
# ── Step 3: Generate root changelog ─────────────────────────────────
|
|
|
|
echo -e "${GREEN}→${NC} Generating root changelog..."
|
|
git-cliff --config "$ROOT_DIR/cliff.toml" --tag "$VERSION_TAG" --output "$CHANGELOG"
|
|
|
|
# ── Step 4: Update per-package changelogs from changesets ─────────────
|
|
# Writes changeset descriptions into each affected crate's CHANGELOG.md
|
|
# under the new version heading. This avoids git-cliff's --include-path
|
|
# limitation where tag boundaries are lost if the tag commit doesn't
|
|
# touch the crate's files.
|
|
|
|
echo -e "${GREEN}→${NC} Updating per-package changelogs..."
|
|
updated=0
|
|
|
|
for pkg in "${!BUMPS[@]}"; do
|
|
local_path="${PKG_PATH[$pkg]:-}"
|
|
[[ -z "$local_path" ]] && continue
|
|
|
|
pkg_dir="$ROOT_DIR/$local_path"
|
|
pkg_changelog="$pkg_dir/CHANGELOG.md"
|
|
new_ver=$(bump_version "$(get_pkg_version "$pkg_dir")" "${BUMPS[$pkg]}")
|
|
today=$(date +%Y-%m-%d)
|
|
|
|
# Collect descriptions from changesets that mention this package
|
|
pkg_entries=""
|
|
for desc in "${DESCRIPTIONS[@]}"; do
|
|
pkg_entries+="- $desc"
|
|
done
|
|
|
|
# If --all with no changesets, add a generic entry
|
|
if [[ -z "$pkg_entries" && -n "$BUMP_ALL" ]]; then
|
|
pkg_entries="- Version bump"$'\n'
|
|
fi
|
|
|
|
# Build the new version section
|
|
version_section="## [$new_ver] - $today"$'\n'$'\n'
|
|
if [[ -n "$pkg_entries" ]]; then
|
|
version_section+="### 🚀 Changes"$'\n'$'\n'
|
|
version_section+="$pkg_entries"$'\n'
|
|
fi
|
|
|
|
if [[ -f "$pkg_changelog" ]]; then
|
|
# Insert new version section after "## [Unreleased]" line
|
|
TEMP_FILE=$(mktemp)
|
|
awk -v section="$version_section" '
|
|
/^## \[Unreleased\]/ {
|
|
print
|
|
print ""
|
|
printf "%s", section
|
|
next
|
|
}
|
|
{ print }
|
|
' "$pkg_changelog" > "$TEMP_FILE"
|
|
mv "$TEMP_FILE" "$pkg_changelog"
|
|
else
|
|
# Create changelog from scratch
|
|
cat > "$pkg_changelog" << EOF
|
|
# Changelog
|
|
|
|
All notable changes to this package will be documented in this file.
|
|
|
|
## [Unreleased]
|
|
|
|
$version_section
|
|
EOF
|
|
fi
|
|
|
|
updated=$((updated + 1))
|
|
echo -e " ${GREEN}✓${NC} $pkg → $new_ver"
|
|
done
|
|
echo -e " Updated ${GREEN}$updated${NC} package changelogs"
|
|
|
|
# ── Step 5: Clean up changeset files ────────────────────────────────
|
|
|
|
shopt -s nullglob
|
|
changeset_files=("$CHANGESET_DIR"/*.md)
|
|
shopt -u nullglob
|
|
cleaned=0
|
|
|
|
for f in "${changeset_files[@]}"; do
|
|
[[ "$(basename "$f")" == "README.md" ]] && continue
|
|
rm "$f"
|
|
cleaned=$((cleaned + 1))
|
|
done
|
|
|
|
if [[ "$cleaned" -gt 0 ]]; then
|
|
echo -e "${GREEN}→${NC} Cleaned up $cleaned changeset files."
|
|
fi
|
|
|
|
# ── Step 6: Commit and tag ──────────────────────────────────────────
|
|
|
|
echo -e "${GREEN}→${NC} Committing release..."
|
|
git -C "$ROOT_DIR" add -A
|
|
git -C "$ROOT_DIR" commit -m "chore(release): $VERSION_TAG"
|
|
git -C "$ROOT_DIR" tag -a "$VERSION_TAG" -m "Release $VERSION_TAG"
|
|
|
|
echo ""
|
|
echo -e "${GREEN}✓ Released $VERSION_TAG${NC}"
|
|
echo ""
|
|
echo -e " Push with: ${CYAN}git push && git push --tags${NC}"
|