diff --git a/README.md b/README.md index 8476005..db267ed 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,22 @@ uv run build.py Artifacts are output to the `dist/` directory, organized by application. +## Releases + +Per-app releases are automated via `scripts/release.sh`. The script only bumps +app versions when relevant files changed since the last `-vX.Y.Z` tag, +then rebuilds, creates per-app tarballs, and publishes Gitea releases. + +Example: + +```bash +./scripts/release.sh 1.0.2 --notes-dir release-notes +``` + +Notes: +- Use `release-notes/.md` or `release-notes/common.md` for release notes. +- Requires a working `tea` login and push access to the repo. + ## Project Structure - `src/`: Source YAML definitions containing the "DNA" of the themes. @@ -62,4 +78,4 @@ Artifacts are output to the `dist/` directory, organized by application. ## Authors -- **S0wlz (Owlibou)** \ No newline at end of file +- **S0wlz (Owlibou)** diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..25d7f64 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,219 @@ +#!/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