|
|
@@ -0,0 +1,170 @@
|
|
|
+#!/usr/bin/env bash
|
|
|
+set -euo pipefail
|
|
|
+
|
|
|
+# Create and publish a Gogs release through the web form.
|
|
|
+#
|
|
|
+# Gogs's API only exposes GET /repos/:owner/:repo/releases; POST returns 404 on
|
|
|
+# this instance. The web form still accepts a normal user/password session, so
|
|
|
+# this helper logs in locally and posts the release. Credentials are read from
|
|
|
+# a mode-600 file or prompt and are never written to the repository or printed.
|
|
|
+#
|
|
|
+# Usage:
|
|
|
+# ./tool/gogs_release.sh <tag> <title> <body-file>
|
|
|
+#
|
|
|
+# Example:
|
|
|
+# ./tool/gogs_release.sh v1.0.7 "CarDash 1.0.7 — Truly disable Impeller" releases/1.0.7.md
|
|
|
+#
|
|
|
+# Optional environment:
|
|
|
+# GOGS_URL default https://git.krasi.net
|
|
|
+# GOGS_REPO default kkazakov/car-interface-android
|
|
|
+# GOGS_TARGET default main
|
|
|
+# GOGS_USER skip the username prompt
|
|
|
+# GOGS_PASSWORD skip the password prompt (prefer the credentials file)
|
|
|
+# GOGS_CREDENTIALS credentials file path (default: $HOME/.config/cardash/gogs_credentials,
|
|
|
+# then android/keystore/gogs_credentials)
|
|
|
+#
|
|
|
+# Credentials file format (mode 600, gitignored):
|
|
|
+# username=kkazakov
|
|
|
+# password=...
|
|
|
+#
|
|
|
+# If 2FA is enabled, log in through the browser and use the web UI instead.
|
|
|
+
|
|
|
+BASE="${GOGS_URL:-https://git.krasi.net}"
|
|
|
+REPO="${GOGS_REPO:-kkazakov/car-interface-android}"
|
|
|
+TARGET="${GOGS_TARGET:-main}"
|
|
|
+
|
|
|
+if [[ $# -lt 2 || $# -gt 3 ]]; then
|
|
|
+ echo "Usage: $0 <tag> <title> [body-file]" >&2
|
|
|
+ echo "Example: $0 v1.0.7 'CarDash 1.0.7' releases/1.0.7.md" >&2
|
|
|
+ exit 2
|
|
|
+fi
|
|
|
+
|
|
|
+TAG="$1"
|
|
|
+TITLE="$2"
|
|
|
+BODY_FILE="${3:-releases/${TAG#v}.md}"
|
|
|
+
|
|
|
+if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
|
+ echo "Tag must look like v1.2.3 (got: $TAG)." >&2
|
|
|
+ exit 2
|
|
|
+fi
|
|
|
+if [[ ! -f "$BODY_FILE" ]]; then
|
|
|
+ echo "Release body file not found: $BODY_FILE" >&2
|
|
|
+ exit 2
|
|
|
+fi
|
|
|
+
|
|
|
+USER_NAME="${GOGS_USER:-}"
|
|
|
+PASSWORD="${GOGS_PASSWORD:-}"
|
|
|
+
|
|
|
+CRED_FILE="${GOGS_CREDENTIALS:-}"
|
|
|
+if [[ -z "$CRED_FILE" ]]; then
|
|
|
+ for candidate in \
|
|
|
+ "$HOME/.config/cardash/gogs_credentials" \
|
|
|
+ "android/keystore/gogs_credentials"; do
|
|
|
+ if [[ -f "$candidate" ]]; then
|
|
|
+ CRED_FILE="$candidate"
|
|
|
+ break
|
|
|
+ fi
|
|
|
+ done
|
|
|
+fi
|
|
|
+if [[ -n "$CRED_FILE" ]]; then
|
|
|
+ if [[ ! -r "$CRED_FILE" ]]; then
|
|
|
+ echo "Credentials file is not readable: $CRED_FILE" >&2
|
|
|
+ exit 2
|
|
|
+ fi
|
|
|
+ if [[ -z "$USER_NAME" ]]; then
|
|
|
+ USER_NAME="$(sed -n 's/^username=//p' "$CRED_FILE" | head -n 1)"
|
|
|
+ fi
|
|
|
+ if [[ -z "$PASSWORD" ]]; then
|
|
|
+ PASSWORD="$(sed -n 's/^password=//p' "$CRED_FILE" | head -n 1)"
|
|
|
+ fi
|
|
|
+fi
|
|
|
+
|
|
|
+if [[ -z "$USER_NAME" ]]; then
|
|
|
+ read -r -p "Gogs user: " USER_NAME
|
|
|
+fi
|
|
|
+
|
|
|
+if [[ -z "$PASSWORD" ]]; then
|
|
|
+ read -r -s -p "Gogs password for $USER_NAME: " PASSWORD
|
|
|
+ echo
|
|
|
+fi
|
|
|
+if [[ -z "$PASSWORD" ]]; then
|
|
|
+ echo "No password supplied." >&2
|
|
|
+ exit 2
|
|
|
+fi
|
|
|
+
|
|
|
+umask 077
|
|
|
+TMP_DIR="$(mktemp -d)"
|
|
|
+cleanup() {
|
|
|
+ rm -rf "$TMP_DIR"
|
|
|
+}
|
|
|
+trap cleanup EXIT
|
|
|
+
|
|
|
+COOKIES="$TMP_DIR/cookies.txt"
|
|
|
+PASS_FILE="$TMP_DIR/password"
|
|
|
+printf '%s' "$PASSWORD" > "$PASS_FILE"
|
|
|
+unset PASSWORD
|
|
|
+
|
|
|
+csrf_from() {
|
|
|
+ sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p' "$1" | head -n 1
|
|
|
+}
|
|
|
+
|
|
|
+LOGIN_PAGE="$TMP_DIR/login.html"
|
|
|
+if ! curl -fsS -c "$COOKIES" "$BASE/user/login" -o "$LOGIN_PAGE"; then
|
|
|
+ echo "Could not reach $BASE/user/login." >&2
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+LOGIN_CSRF="$(csrf_from "$LOGIN_PAGE")"
|
|
|
+if [[ -z "$LOGIN_CSRF" ]]; then
|
|
|
+ echo "Could not read the Gogs login form. Is GOGS_URL correct?" >&2
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+
|
|
|
+LOGIN_CODE="$(curl -sS -b "$COOKIES" -c "$COOKIES" \
|
|
|
+ -o "$TMP_DIR/login-result.html" -w '%{http_code}' \
|
|
|
+ -X POST "$BASE/user/login" \
|
|
|
+ --data-urlencode "_csrf=$LOGIN_CSRF" \
|
|
|
+ --data-urlencode "user_name=$USER_NAME" \
|
|
|
+ --data-urlencode "password@$PASS_FILE" \
|
|
|
+ --data-urlencode 'remember=on')"
|
|
|
+if [[ "$LOGIN_CODE" != "302" ]]; then
|
|
|
+ echo "Gogs login failed (HTTP $LOGIN_CODE). Check the username/password or 2FA." >&2
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+
|
|
|
+NEW_PAGE="$TMP_DIR/new-release.html"
|
|
|
+if ! curl -fsS -b "$COOKIES" -c "$COOKIES" \
|
|
|
+ "$BASE/$REPO/releases/new" -o "$NEW_PAGE"; then
|
|
|
+ echo "Could not open $BASE/$REPO/releases/new. Is the session valid?" >&2
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+if ! grep -q 'name="tag_name"' "$NEW_PAGE"; then
|
|
|
+ echo "The release form did not load; the account may lack write access." >&2
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+RELEASE_CSRF="$(csrf_from "$NEW_PAGE")"
|
|
|
+if [[ -z "$RELEASE_CSRF" ]]; then
|
|
|
+ echo "Could not read the release form CSRF token." >&2
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+
|
|
|
+RELEASE_CODE="$(curl -sS -b "$COOKIES" -c "$COOKIES" \
|
|
|
+ -D "$TMP_DIR/release-headers.txt" -o "$TMP_DIR/release-result.html" \
|
|
|
+ -w '%{http_code}' \
|
|
|
+ -X POST "$BASE/$REPO/releases/new" \
|
|
|
+ --data-urlencode "_csrf=$RELEASE_CSRF" \
|
|
|
+ --data-urlencode "tag_name=$TAG" \
|
|
|
+ --data-urlencode "tag_target=$TARGET" \
|
|
|
+ --data-urlencode "title=$TITLE" \
|
|
|
+ --data-urlencode "content@$BODY_FILE")"
|
|
|
+
|
|
|
+if [[ "$RELEASE_CODE" != "302" ]]; then
|
|
|
+ echo "Gogs rejected the release (HTTP $RELEASE_CODE)." >&2
|
|
|
+ echo "Check that $TAG does not already exist and that $TARGET is a branch." >&2
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+
|
|
|
+if grep -q "\"tag_name\": *\"$TAG\"" <(curl -fsS "$BASE/api/v1/repos/$REPO/releases"); then
|
|
|
+ echo "Published $TAG"
|
|
|
+else
|
|
|
+ echo "Release request accepted; verify at $BASE/$REPO/releases"
|
|
|
+fi
|