chore: add release automation
This commit is contained in:
219
scripts/release.sh
Executable file
219
scripts/release.sh
Executable file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/release.sh <version> [options]
|
||||
|
||||
Options:
|
||||
--repo <slug> Gitea repo slug (default: auto-detect)
|
||||
--login <name> tea login name (default: tea default)
|
||||
--notes <text> Release notes (single line)
|
||||
--notes-file <file> Release notes file (applies to all apps)
|
||||
--notes-dir <dir> Per-app notes dir; uses <app>.md or common.md
|
||||
--no-push Skip git push
|
||||
--dry-run Print actions without executing
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
version="${1:-}"
|
||||
shift || true
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "error: version must be semver (e.g., 1.2.3)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo=""
|
||||
login=""
|
||||
notes=""
|
||||
notes_file=""
|
||||
notes_dir=""
|
||||
no_push=0
|
||||
dry_run=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo)
|
||||
repo="$2"
|
||||
shift 2
|
||||
;;
|
||||
--login)
|
||||
login="$2"
|
||||
shift 2
|
||||
;;
|
||||
--notes)
|
||||
notes="$2"
|
||||
shift 2
|
||||
;;
|
||||
--notes-file)
|
||||
notes_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--notes-dir)
|
||||
notes_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-push)
|
||||
no_push=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "error: unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
run() {
|
||||
if (( dry_run )); then
|
||||
echo "+ $*"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "error: not inside a git repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo "error: working tree not clean; commit or stash first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v tea >/dev/null 2>&1; then
|
||||
echo "error: tea is not installed or not on PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t apps < <(python - <<'PY'
|
||||
from pathlib import Path
|
||||
apps = sorted({p.parent.name for p in Path("templates").rglob("meta.yaml")})
|
||||
print("\n".join(apps))
|
||||
PY
|
||||
)
|
||||
|
||||
changed_apps=()
|
||||
for app in "${apps[@]}"; do
|
||||
last_tag="$(git tag --list "${app}-v*" --sort=-version:refname | head -n 1)"
|
||||
if [[ -z "$last_tag" ]]; then
|
||||
changed_apps+=("$app")
|
||||
continue
|
||||
fi
|
||||
if git diff --name-only "${last_tag}..HEAD" | grep -Eq "^(src/|build.py$|templates/${app}/)"; then
|
||||
changed_apps+=("$app")
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#changed_apps[@]} == 0 )); then
|
||||
echo "no app changes detected since last per-app tags; nothing to release"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if (( dry_run )); then
|
||||
echo "+ update meta.yaml for: ${changed_apps[*]}"
|
||||
else
|
||||
APP_LIST="$(printf "%s\n" "${changed_apps[@]}")" APP_VERSION="${version}" python - <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
new_version = os.environ["APP_VERSION"]
|
||||
targets = set(filter(None, os.environ["APP_LIST"].splitlines()))
|
||||
for path in Path("templates").rglob("meta.yaml"):
|
||||
if path.parent.name not in targets:
|
||||
continue
|
||||
text = path.read_text()
|
||||
lines = text.splitlines()
|
||||
out = []
|
||||
changed = False
|
||||
for line in lines:
|
||||
if line.startswith("version:"):
|
||||
out.append(f"version: {new_version}")
|
||||
changed = True
|
||||
else:
|
||||
out.append(line)
|
||||
if changed:
|
||||
path.write_text("\\n".join(out) + "\\n")
|
||||
PY
|
||||
fi
|
||||
|
||||
if ! git diff --quiet; then
|
||||
run git add templates/*/meta.yaml
|
||||
run git commit -m "chore: release apps v${version}"
|
||||
fi
|
||||
|
||||
run uv run build.py
|
||||
run mkdir -p dist/releases
|
||||
|
||||
for app in "${changed_apps[@]}"; do
|
||||
run tar -czf "dist/releases/apex-${app}-v${version}.tar.gz" -C dist "$app"
|
||||
done
|
||||
|
||||
if (( no_push == 0 )); then
|
||||
run git push origin HEAD
|
||||
fi
|
||||
|
||||
commit="$(git rev-parse HEAD)"
|
||||
|
||||
note_args=()
|
||||
pick_note_args() {
|
||||
local app="$1"
|
||||
note_args=()
|
||||
if [[ -n "$notes_file" ]]; then
|
||||
note_args=(--note-file "$notes_file")
|
||||
return
|
||||
fi
|
||||
if [[ -n "$notes" ]]; then
|
||||
note_args=(--note "$notes")
|
||||
return
|
||||
fi
|
||||
if [[ -n "$notes_dir" ]]; then
|
||||
if [[ -f "$notes_dir/$app.md" ]]; then
|
||||
note_args=(--note-file "$notes_dir/$app.md")
|
||||
return
|
||||
fi
|
||||
if [[ -f "$notes_dir/common.md" ]]; then
|
||||
note_args=(--note-file "$notes_dir/common.md")
|
||||
return
|
||||
fi
|
||||
fi
|
||||
note_args=(--note "Release v${version}")
|
||||
}
|
||||
|
||||
for app in "${changed_apps[@]}"; do
|
||||
tag="${app}-v${version}"
|
||||
title="Apex ${app} v${version}"
|
||||
asset="dist/releases/apex-${app}-v${version}.tar.gz"
|
||||
|
||||
args=(tea releases create)
|
||||
[[ -n "$repo" ]] && args+=(--repo "$repo")
|
||||
[[ -n "$login" ]] && args+=(--login "$login")
|
||||
args+=(--tag "$tag" --target "$commit" --title "$title")
|
||||
pick_note_args "$app"
|
||||
args+=("${note_args[@]}" --asset "$asset")
|
||||
|
||||
run "${args[@]}"
|
||||
done
|
||||
Reference in New Issue
Block a user