#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' usage() { cat <<'EOF' Usage: scripts/release.sh [options] Options: --repo Gitea repo slug (default: auto-detect) --login tea login name (default: tea default) --notes Release notes (single line) --notes-file Release notes file (applies to all apps) --notes-dir Per-app notes dir; uses .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