From b2173e82c8df880164a879f8e5d573abdc40bac9 Mon Sep 17 00:00:00 2001 From: foxyz Date: Sat, 12 Sep 2026 11:57:36 +0300 Subject: [PATCH] Add tool/package_linux.sh: release tar.gz + Discord upload card One command: flutter build linux --release, then bundles into dist/watch-together--linux-x64.tar.gz with a single top-level folder (executable, lib/, data/). Uploads via DISCORD_WEBHOOK_URL env (never committed) as a Components V2 card: version header, short git commit, stat codeblock, tarball as File component. --noupload skips, --skip-build reuses the last bundle. Verified: 0.1.0 builds, exec bit preserved, payload asserted against a stub (flag 32768, container+file shape, attachment ref). --- tool/package_linux.sh | 121 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100755 tool/package_linux.sh diff --git a/tool/package_linux.sh b/tool/package_linux.sh new file mode 100755 index 0000000..79e393e --- /dev/null +++ b/tool/package_linux.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Package the Linux release bundle into a versioned tar.gz and optionally +# upload it to Discord. +# Usage: tool/package_linux.sh [--skip-build] [--noupload|--no-upload] +# Output: dist/watch-together--linux-x64.tar.gz containing one +# top-level folder with the executable, lib/ and data/ as built. +# Upload: set DISCORD_WEBHOOK_URL in the environment (never commit it). +# The tarball must stay under Discord's upload limit (25 MB free tier). +# The upload posts a Components V2 card (flag 1<<15): version header, +# short git commit, stat codeblock, and the tarball as a File component. +# Needs: curl, git, python3 (JSON payload building). +set -euo pipefail + +SKIP_BUILD=0 +UPLOAD=1 +for arg in "$@"; do + case "$arg" in + --skip-build) SKIP_BUILD=1 ;; + --noupload|--no-upload) UPLOAD=0 ;; + *) echo "usage: $0 [--skip-build] [--noupload|--no-upload]" >&2; exit 1 ;; + esac +done + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +APP="$ROOT/apps/desktop" +NAME="watch-together" +ARCH="x64" + +VERSION="$(sed -n 's/^version: \([0-9.]*\).*/\1/p' "$APP/pubspec.yaml")" +if [[ -z "$VERSION" ]]; then echo "error: version not found in $APP/pubspec.yaml" >&2; exit 1; fi + +if [[ "$SKIP_BUILD" != "1" ]]; then + (cd "$APP" && flutter build linux --release) +fi + +BUNDLE="$APP/build/linux/$ARCH/release/bundle" +[[ -d "$BUNDLE" ]] || { echo "error: bundle missing (build first): $BUNDLE" >&2; exit 1; } + +STAGE="$APP/build/package-stage" +DIST="$ROOT/dist" +FOLDER="$NAME-$VERSION" +TARBALL="$DIST/$FOLDER-linux-$ARCH.tar.gz" +rm -rf "$STAGE" +mkdir -p "$STAGE/$FOLDER" "$DIST" +cp -r "$BUNDLE/." "$STAGE/$FOLDER/" +tar -czf "$TARBALL" -C "$STAGE" "$FOLDER" +rm -rf "$STAGE" +ls -la "$TARBALL" + +if [[ "$UPLOAD" == "1" ]]; then + if [[ -z "${DISCORD_WEBHOOK_URL:-}" ]]; then + echo "warning: DISCORD_WEBHOOK_URL not set — packaged but not uploaded." >&2 + else + command -v curl >/dev/null || { echo "error: curl not found" >&2; exit 1; } + command -v python3 >/dev/null || { echo "error: python3 not found" >&2; exit 1; } + PAYLOAD="$(mktemp)" + trap 'rm -f "$PAYLOAD"' EXIT + export PKG_ROOT="$ROOT" PKG_PAYLOAD="$PAYLOAD" PKG_TARBALL="$TARBALL" \ + PKG_FOLDER="$FOLDER" PKG_VERSION="$VERSION" PKG_ARCH="$ARCH" + python3 <<'EOF' +import json, os, subprocess + +root = os.environ['PKG_ROOT'] +out = os.environ['PKG_PAYLOAD'] +tarball = os.environ['PKG_TARBALL'] +fname = os.path.basename(tarball) +folder = os.environ['PKG_FOLDER'] +ver = os.environ['PKG_VERSION'] +arch = os.environ['PKG_ARCH'] +size_mb = os.path.getsize(tarball) / 1048576 + +def git(*args): + try: + p = subprocess.run(['git', '-C', root, *args], + capture_output=True, text=True, timeout=10) + return p.stdout.strip() + except Exception: + return '' + +commit = git('log', '-1', '--format=%h %s') or 'uncommitted build' +stat = git('show', '--stat', '--oneline', '--format=', 'HEAD') +stat_lines = [l for l in stat.splitlines() if l.strip()][:14] +rest = len([l for l in stat.splitlines() if l.strip()]) - len(stat_lines) +if rest > 0: + stat_lines.append(f'... +{rest} more files') +block = '```\n' + '\n'.join(stat_lines) + '\n```' + +payload = { + 'flags': 1 << 15, + 'attachments': [{ + 'id': 0, + 'filename': fname, + 'description': f'{folder} linux {arch} release build', + }], + 'components': [{ + 'type': 17, + 'accent_color': 5793266, + 'components': [ + {'type': 10, 'content': + f'# 📦 watch-together `{ver}`\n' + f'-# linux · {arch} · release · {size_mb:.1f} MB'}, + {'type': 10, 'content': f'`{commit[:140]}`'}, + {'type': 10, 'content': block[:1500]}, + {'type': 14, 'divider': True, 'spacing': 1}, + {'type': 13, 'file': {'url': f'attachment://{fname}'}}, + ], + }], +} +with open(out, 'w') as f: + json.dump(payload, f) +EOF + # Plain (non-application) webhooks ignore `components` unless the + # with_components query param is set — without it Discord 500s. + SEP='?'; [[ "$DISCORD_WEBHOOK_URL" == *\?* ]] && SEP='&' + curl -fsS \ + -F "payload_json=<$PAYLOAD;type=application/json" \ + -F "files[0]=@$TARBALL;type=application/gzip" \ + "${DISCORD_WEBHOOK_URL}${SEP}with_components=true" > /dev/null + echo "uploaded $FOLDER-linux-$ARCH.tar.gz" + fi +fi