added new auto-update scripts

This commit is contained in:
crowetic 2025-07-10 13:47:10 -07:00
parent 87897d7db8
commit 6c0a9b3539
4 changed files with 866 additions and 0 deletions

View File

@ -0,0 +1,130 @@
# Qortal Auto-Update Publisher Scripts
This toolkit modernizes and automates the Qortal auto-update process. It includes:
- A Bash script (`build-auto-update.sh`) to build and push the update
- A Python script (`publish-auto-update.py`) to publish the auto-update transaction
- Full support for dry-run mode, interactive or scripted use, and secure key input
---
## 🧰 Prerequisites
- You must be a **non-admin member** of the Qortal `dev` group
- A Qortal core node must be running locally (default API port: `12391`)
- You need the latest version of the `qortal` repo cloned locally
---
## 🚀 Workflow Overview
### 1. Run the Build Script
This script:
- Auto-increments the version in `pom.xml`
- Rebuilds the JAR file
- XORs it into a `.update` file
- Creates a new `auto-update-<hash>` branch with only the update
- Pushes it to the repo
```bash
./tools/auto-update-scripts/build-auto-update.sh
```
You'll be prompted to:
- Confirm or modify the version number
- Push the version tag and update branch, and final commit.
- Optionally run the publisher script at the end
> ✅ Dry-run mode is supported to preview the full process.
---
### 2. Publish the Auto-Update
You can either:
- Let the build script call it for you
- Or run it manually:
```bash
# Run manually with interactive key prompt and auto-detected latest update:
python3 tools/auto-update-scripts/publish-auto-update.py
# Or specify a commit hash:
python3 tools/auto-update-scripts/publish-auto-update.py 0b37666d
# Or pass both from another script:
python3 tools/auto-update-scripts/publish-auto-update.py <privkey> <commit_hash>
```
> 🔐 Private key is always prompted securely unless passed explicitly (e.g. from automation).
This script will:
- Detect the latest `auto-update-<hash>` branch (or use the one you specify)
- Validate that the commit exists
- Restore the `.update` file if missing
- Compute its SHA256 hash
- Build and sign the transaction
- Submit it to your local node
> ✅ `--dry-run` is supported to show what would happen without sending anything.
---
## 🛠 Advanced Options
- Log files are created in `~/qortal-auto-update-logs` by default
- You can override the log directory interactively
- Branch naming is standardized: `auto-update-<short-commit-hash>`
- The `.update` file is XOR-obfuscated using Qortals built-in logic
- Your commit must already exist on the main repo (e.g. via push or PR merge)
---
## 📌 Notes
- **Do not use Git LFS** — Qortal nodes download `.update` files using raw HTTP from GitHub
We may build LFS support in the future, but for now it is NOT utilized, and will NOT work.
(Other locations for the publish of the .update file will be utilized in the future,
preferably utilizing QDN via gateway nodes, until auto-update setup can be re-written to
leverage QDN directly.)
- GitHub will warn if `.update` files exceed 50MB, but auto-update still works.
(In the past there HAVE been issues with accounts getting banned due to publish of .update file,
however, as of recently (April 2025) it seems they are only warning, and not banning. But we
will be modifying the need for this in the future anyway.)
- Update mirrors will be added in the future, and others can be added in settings as well.
---
## ✅ Example End-to-End (Manual)
```bash
cd ~/git-repos/qortal
./tools/auto-update-scripts/build-auto-update.sh
# follow prompts...
# then manually publish:
python3 tools/auto-update-scripts/publish-auto-update.py
```
---
## 🧪 Test Without Sending
```bash
./build-auto-update.sh # enable dry-run when prompted
# OR
python3 publish-auto-update.py 0b37666d --dry-run
```
---
## 🙌 Contributors
Modernization by [@crowetic](https://github.com/crowetic)
Based on original Perl scripts by Qortal core devs, specifically @catbref.
---
Questions or issues? Drop into the Qortal Dev group on Discord, Q-Chat, or reach out directly via Q-Mail to 'crowetic'.

View File

@ -0,0 +1,264 @@
#!/usr/bin/env bash
set -euo pipefail
# === Configurable Defaults ===
BASE_BRANCH="master"
DEFAULT_LOG_DIR="${HOME}/qortal-auto-update-logs"
LOG_FILE=""
DRY_RUN=false
RUN_PUBLISH=false
PUBLISH_SCRIPT="tools/auto-update-scripts/publish-auto-update.py"
# === Helper Functions ===
function abort() {
echo -e "\nERROR: $1" >&2
exit 1
}
function confirm_or_exit() {
echo "$1"
read -rp "Continue? (y/N): " confirm
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 1
}
function run_git() {
echo "Running: git $*" | tee -a "$LOG_FILE"
$DRY_RUN || git "$@"
}
function increment_version() {
local version=$1
local major minor patch
IFS='.' read -r major minor patch <<< "$version"
((patch++))
echo "$major.$minor.$patch"
}
# === Prompt for Logging Directory ===
echo "Default log directory: ${DEFAULT_LOG_DIR}"
read -rp "Use this log directory? (Y/n): " log_choice
if [[ "${log_choice}" =~ ^[Nn]$ ]]; then
read -rp "Enter desired log directory path: " CUSTOM_LOG_DIR
LOG_DIR="${CUSTOM_LOG_DIR}"
else
LOG_DIR="${DEFAULT_LOG_DIR}"
fi
mkdir -p "${LOG_DIR}" || abort "Unable to create log directory: ${LOG_DIR}"
LOG_FILE="${LOG_DIR}/qortal-auto-update-log-$(date +%Y%m%d-%H%M%S).log"
echo "Logging to: ${LOG_FILE}"
# Log everything to file as well as terminal
exec > >(tee -a "$LOG_FILE") 2>&1
# === Dry Run Mode Option ===
read -rp "Enable dry-run mode? (y/N): " dry_choice
if [[ "${dry_choice}" =~ ^[Yy]$ ]]; then
DRY_RUN=true
echo "Dry-run mode ENABLED. Commands will be shown but not executed."
else
echo "Dry-run mode DISABLED. Real commands will be executed."
fi
# === Run Python Publisher Option ===
read -rp "Run the Python publish_auto_update script at the end? (y/N): " pub_choice
if [[ "${pub_choice}" =~ ^[Yy]$ ]]; then
RUN_PUBLISH=true
read -rp "Run Python script in dry-run mode? (y/N): " pub_dry
if [[ "${pub_dry}" =~ ^[Yy]$ ]]; then
PUBLISH_DRY_FLAG="--dry-run"
else
PUBLISH_DRY_FLAG=""
fi
else
RUN_PUBLISH=false
fi
# === Detect Git Root ===
git_dir=$(git rev-parse --show-toplevel 2>/dev/null || true)
[[ -z "${git_dir}" ]] && abort "Not inside a git repository."
cd "${git_dir}"
echo
echo "Current Git identity:"
git config user.name || echo "(not set)"
git config user.email || echo "(not set)"
read -rp "Would you like to set/override the Git username and email for this repo? (y/N): " git_id_choice
if [[ "${git_id_choice}" =~ ^[Yy]$ ]]; then
read -rp "Enter Git username (e.g. Qortal-Auto-Update): " git_user
read -rp "Enter Git email (e.g. qortal-auto-update@example.com): " git_email
run_git config user.name "${git_user}"
run_git config user.email "${git_email}"
echo "Git identity set to: ${git_user} <${git_email}>"
fi
# === Confirm Git Origin URL ===
git_origin=$(git config --get remote.origin.url)
echo "Git origin URL: ${git_origin}"
confirm_or_exit "Is this the correct repository?"
# === Verify Current Branch ===
current_branch=$(git rev-parse --abbrev-ref HEAD)
echo "Current git branch: ${current_branch}"
if [[ "${current_branch}" != "${BASE_BRANCH}" ]]; then
echo "Expected to be on '${BASE_BRANCH}' branch, but found '${current_branch}'"
confirm_or_exit "Proceed anyway in 5 seconds or abort with CTRL+C."
sleep 5
fi
# === Check for Uncommitted Changes ===
uncommitted=$(git status --short --untracked-files=no)
if [[ -n "${uncommitted}" ]]; then
echo "Uncommitted changes detected:"
echo "${uncommitted}"
abort "Please commit or stash changes first."
fi
project=$(grep -oPm1 "(?<=<artifactId>)[^<]+" pom.xml)
[[ -z "${project}" ]] && abort "Unable to determine project name from pom.xml."
echo "Detected project: ${project}"
# === Auto-Increment Version in pom.xml ===
current_version=$(grep -oPm1 "(?<=<version>)[^<]+" pom.xml)
new_version=$(increment_version "$current_version")
$DRY_RUN || sed -i "s|<version>${current_version}</version>|<version>${new_version}</version>|" pom.xml
echo "Updated version from ${current_version} to ${new_version} in pom.xml"
git diff pom.xml
while true; do
read -rp "Is the updated version correct? (y/N): " version_ok
if [[ "${version_ok}" =~ ^[Yy]$ ]]; then
break
fi
read -rp "Enter the correct version number (e.g., 4.7.2): " user_version
# Validate format x.y.z and version > current_version
if [[ ! "${user_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid format. Use x.y.z (e.g., 4.7.2)."
continue
fi
IFS='.' read -r curr_major curr_minor curr_patch <<< "${current_version}"
IFS='.' read -r new_major new_minor new_patch <<< "${user_version}"
if (( new_major < curr_major )) || \
(( new_major == curr_major && new_minor < curr_minor )) || \
(( new_major == curr_major && new_minor == curr_minor && new_patch <= curr_patch )); then
echo "Version must be greater than current version (${current_version})."
continue
fi
$DRY_RUN || sed -i "s|<version>${new_version}</version>|<version>${user_version}</version>|" pom.xml
echo "Updated version to user-provided version: ${user_version}"
git diff pom.xml
new_version="${user_version}"
echo
echo "Rechecking updated version..."
done
run_git add pom.xml
run_git commit -m "Bump version to ${new_version}"
run_git tag "v${new_version}"
confirm_or_exit "About to push version tag 'v${new_version}' to origin."
run_git push origin "v${new_version}"
confirm_or_exit "Also push the ${current_branch} branch to origin?"
run_git push origin "${current_branch}"
# === Extract Info ===
short_hash=$(git rev-parse --short HEAD)
[[ -z "${short_hash}" ]] && abort "Unable to extract commit hash."
echo "Using commit hash: ${short_hash}"
# === Build JAR ===
echo "Building JAR for ${project}..."
if ! $DRY_RUN; then
mvn clean package > /dev/null 2>&1 || {
echo "Build failed. Check logs in ${LOG_FILE}" >&2
abort "Maven build failed."
}
fi
jar_file=$(ls target/${project}*.jar | head -n1)
[[ ! -f "${jar_file}" ]] && abort "Built JAR file not found."
# === XOR Obfuscation ===
echo "Creating ${project}.update..."
$DRY_RUN || java -cp "${jar_file}" org.qortal.XorUpdate "${jar_file}" "${project}.update"
# === Create Auto-Update Branch ===
update_branch="auto-update-${short_hash}"
echo "Creating update branch: ${update_branch}"
if git show-ref --verify --quiet refs/heads/${update_branch}; then
run_git branch -D "${update_branch}"
fi
run_git checkout --orphan "${update_branch}"
$DRY_RUN || git rm -rf . > /dev/null 2>&1 || true
run_git add "${project}.update"
run_git commit -m "XORed auto-update JAR for commit ${short_hash}"
confirm_or_exit "About to push auto-update branch '${update_branch}' to origin."
run_git push --set-upstream origin "${update_branch}"
# === Return to Original Branch ===
echo "Switching back to original branch: ${current_branch}"
run_git checkout --force "${current_branch}"
echo "Done. ${project}.update is committed to ${update_branch}."
# === Summary Output ===
echo
echo "======================================"
echo "✅ Auto-Update Build Complete!"
echo "--------------------------------------"
echo "Project: ${project}"
echo "Version: ${new_version}"
echo "Tag: v${new_version}"
echo "Commit Hash: ${short_hash}"
echo "Auto-Update Branch: auto-update-${short_hash}"
echo
echo "Pushed to: ${git_origin}"
echo "Logs saved to: ${LOG_FILE}"
echo "======================================"
echo
# === Provide additional information regarding publish script, and private key. ===
if $RUN_PUBLISH; then
echo "...===...===...===...===...===..."
echo
echo "CONTINUING TO EXECUTE PUBLISH SCRIPT AS SELECTED"
echo
echo "This will publish the AUTO-UPDATE TRANSACTION for signing by the DEVELOPER GROUP ADMINS"
echo
echo "NOTICE: For security, when prompted for PRIVATE KEY, you will NOT see the input, SIMPLY PASTE/TYPE KEY AND PUSH ENTER."
echo
echo "...===...===...===...===...===..."
fi
# === Optionally Run Python Publisher ===
if $RUN_PUBLISH; then
echo "Running Python publish_auto_update script..."
if [[ -f "${PUBLISH_SCRIPT}" ]]; then
read -rsp "Enter your Base58 private key: " PRIVATE_KEY
echo
if [[ "${PUBLISH_DRY_FLAG}" == "--dry-run" ]]; then
echo "Dry-run mode active for Python script."
python3 "${PUBLISH_SCRIPT}" "${PRIVATE_KEY}" "${short_hash}" --dry-run
else
echo "Publishing auto-update for real..."
python3 "${PUBLISH_SCRIPT}" "${PRIVATE_KEY}" "${short_hash}"
fi
else
echo "WARNING: Python script not found at ${PUBLISH_SCRIPT}. Skipping."
fi
fi

View File

@ -0,0 +1,238 @@
#!/bin/bash
# Check if version argument is passed
if [ -z "$1" ]; then
echo "Usage: $0 <VERSION>"
exit 1
fi
VERSION="$1"
# Repository and branch information
REPO="Qortal/qortal"
BRANCH="master"
WORKING_QORTAL_DIR='./qortal'
# 1. Check if working directory exists
if [ ! -d "$WORKING_QORTAL_DIR" ]; then
echo "Error: Working directory '$WORKING_QORTAL_DIR' not found."
read -p "Would you like to: (1) Create a new directory here, or (2) Specify a full path? [1/2]: " choice
if [ "$choice" = "1" ]; then
mkdir -p "$WORKING_QORTAL_DIR"
echo "Created new directory: $WORKING_QORTAL_DIR"
elif [ "$choice" = "2" ]; then
read -p "Enter full path to working directory: " new_path
WORKING_QORTAL_DIR="$new_path"
echo "Using specified directory: $WORKING_QORTAL_DIR"
else
echo "Invalid choice. Exiting."
exit 1
fi
fi
# 2. Check for qortal.jar
JAR_FILE="$WORKING_QORTAL_DIR/qortal.jar"
if [ ! -f "$JAR_FILE" ]; then
echo "Error: $JAR_FILE not found."
read -p "Would you like to: (1) Compile from source, (2) Use running qortal.jar, or (3) Specify a path? [1/2/3]: " choice
if [ "$choice" = "1" ]; then
echo "Cloning repo and compiling..."
git clone https://github.com/Qortal/qortal.git /tmp/qortal
if ! command -v mvn &> /dev/null; then
echo "Error: Maven not found. Please install Maven and try again."
exit 1
fi
cd /tmp/qortal || exit
mvn clean package
cp target/qortal-*.jar "$WORKING_QORTAL_DIR/qortal.jar"
cd - || exit
elif [ "$choice" = "2" ]; then
if [ -f "$HOME/qortal/qortal.jar" ]; then
cp "$HOME/qortal/qortal.jar" "$WORKING_QORTAL_DIR/"
echo "Copied from $HOME/qortal/qortal.jar"
else
echo "Error: $HOME/qortal/qortal.jar not found."
exit 1
fi
elif [ "$choice" = "3" ]; then
read -p "Enter full path to qortal.jar: " jar_path
cp "$jar_path" "$WORKING_QORTAL_DIR/"
echo "Used specified path: $jar_path"
else
echo "Invalid choice. Exiting."
exit 1
fi
fi
# 3. Check for required files (settings.json, log4j2.properties, etc.)
REQUIRED_FILES=("settings.json" "log4j2.properties" "start.sh" "stop.sh" "qort")
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$WORKING_QORTAL_DIR/$file" ]; then
echo "Error: $WORKING_QORTAL_DIR/$file not found."
read -p "Would you like to: (1) Get files from GitHub (2) exit and copy files manually then re-run? [1/2]: " choice
if [ "$choice" = "1" ]; then
if [ "$file" = "settings.json" ]; then
cat <<EOF > "$WORKING_QORTAL_DIR/settings.json"
{
"balanceRecorderEnabled": true,
"apiWhitelistEnabled": false,
"allowConnectionsWithOlderPeerVersions": false,
"apiRestricted": false
}
EOF
elif [ "${file}" = "qort" ]; then
echo "Downloading from GitHub..."
curl -s "https://raw.githubusercontent.com/Qortal/qortal/refs/heads/$BRANCH/tools/$file" -o "$WORKING_QORTAL_DIR/$file"
echo "Making $file script executable..."
chmod +x "$WORKING_QORTAL_DIR/$file"
elif [ "${file}" = "start.sh" ]; then
echo "Downloading from GitHub..."
curl -s "https://raw.githubusercontent.com/Qortal/qortal/refs/heads/$BRANCH/$file" -o "$WORKING_QORTAL_DIR/$file"
echo "Making $file script executable..."
chmod +x "$WORKING_QORTAL_DIR/$file"
elif [ "${file}" = "stop.sh" ]; then
echo "Downloading from GitHub..."
curl -s "https://raw.githubusercontent.com/Qortal/qortal/refs/heads/$BRANCH/$file" -o "$WORKING_QORTAL_DIR/$file"
echo "Making $file script executable..."
chmod +x "$WORKING_QORTAL_DIR/$file"
else
echo "Downloading from GitHub..."
curl -s "https://raw.githubusercontent.com/Qortal/qortal/refs/heads/$BRANCH/$file" -o "$WORKING_QORTAL_DIR/$file"
fi
elif [ "$choice" = "2" ]; then
echo "copy files manually to this location then re-run script..."
sleep 5
exit 1
else
echo "Invalid choice. Exiting."
exit 1
fi
fi
done
# Continue with the rest of the script...
# (The rest of the script remains unchanged)
# Fetch the latest 100 commits
COMMITS_JSON=$(curl -s "https://api.github.com/repos/${REPO}/commits?sha=${BRANCH}&per_page=100")
# Extract bump version commits
BUMP_COMMITS=$(echo "$COMMITS_JSON" | jq -r '.[] | select(.commit.message | test("bump version to"; "i")) | .sha')
CURRENT_BUMP_COMMIT=$(echo "$COMMITS_JSON" | jq -r ".[] | select(.commit.message | test(\"bump version to ${VERSION}\"; \"i\")) | .sha" | head -n1)
PREV_BUMP_COMMIT=$(echo "$BUMP_COMMITS" | sed -n '2p')
if [ -z "$CURRENT_BUMP_COMMIT" ]; then
echo "Error: Could not find bump commit for version ${VERSION} in ${REPO}/${BRANCH}"
exit 1
fi
# Get changelog between previous and current commit
echo "Generating changelog between ${PREV_BUMP_COMMIT} and ${CURRENT_BUMP_COMMIT}..."
CHANGELOG=$(curl -s "https://api.github.com/repos/${REPO}/compare/${PREV_BUMP_COMMIT}...${CURRENT_BUMP_COMMIT}" | jq -r '.commits[] | "- " + .sha[0:7] + " " + .commit.message')
# Fetch latest commit timestamp from GitHub API for final file timestamping
COMMIT_API_URL="https://api.github.com/repos/${REPO}/commits?sha=${BRANCH}&per_page=1"
COMMIT_TIMESTAMP=$(curl -s "${COMMIT_API_URL}" | jq -r '.[0].commit.committer.date')
if [ -z "${COMMIT_TIMESTAMP}" ] || [ "${COMMIT_TIMESTAMP}" == "null" ]; then
echo "Error: Unable to retrieve the latest commit timestamp from GitHub API."
exit 1
fi
# Define file names
JAR_FILE="qortal/qortal.jar"
EXE_FILE="qortal.exe"
ZIP_FILE="qortal.zip"
calculate_hashes() {
local file="$1"
echo "Calculating hashes for ${file}..."
MD5=$(md5sum "${file}" | awk '{print $1}')
SHA1=$(sha1sum "${file}" | awk '{print $1}')
SHA256=$(sha256sum "${file}" | awk '{print $1}')
echo "MD5: ${MD5}, SHA1: ${SHA1}, SHA256: ${SHA256}"
}
# Hashes for qortal.jar
if [ -f "${JAR_FILE}" ]; then
calculate_hashes "${JAR_FILE}"
JAR_MD5=${MD5}
JAR_SHA1=${SHA1}
JAR_SHA256=${SHA256}
else
echo "Error: ${JAR_FILE} not found."
exit 1
fi
# Hashes for qortal.exe
if [ -f "${EXE_FILE}" ]; then
calculate_hashes "${EXE_FILE}"
EXE_MD5=${MD5}
EXE_SHA1=${SHA1}
EXE_SHA256=${SHA256}
else
echo "Warning: ${EXE_FILE} not found. Skipping."
EXE_MD5="<INPUT>"
EXE_SHA1="<INPUT>"
EXE_SHA256="<INPUT>"
fi
# Apply commit timestamp to files in qortal/
echo "Applying commit timestamp (${COMMIT_TIMESTAMP}) to files..."
mv qortal.exe ${WORKING_QORTAL_DIR} 2>/dev/null || true
find ${WORKING_QORTAL_DIR} -type f -exec touch -d "${COMMIT_TIMESTAMP}" {} \;
mv ${WORKING_QORTAL_DIR}/qortal.exe . 2>/dev/null || true
# Create qortal.zip
echo "Packing ${ZIP_FILE}..."
7z a -r -tzip "${ZIP_FILE}" ${WORKING_QORTAL_DIR}/ -stl
if [ $? -ne 0 ]; then
echo "Error: Failed to create ${ZIP_FILE}."
exit 1
fi
calculate_hashes "${ZIP_FILE}"
ZIP_MD5=${MD5}
ZIP_SHA1=${SHA1}
ZIP_SHA256=${SHA256}
# Generate release notes
cat <<EOF > release-notes.txt
### **_Qortal Core V${VERSION}_**
#### 🔄 Changes Included in This Release:
${CHANGELOG}
### [qortal.jar](https://github.com/Qortal/qortal/releases/download/v${VERSION}/qortal.jar)
\`MD5: ${JAR_MD5}\` qortal.jar
\`SHA1: ${JAR_SHA1}\` qortal.jar
\`SHA256: ${JAR_SHA256}\` qortal.jar
### [qortal.exe](https://github.com/Qortal/qortal/releases/download/v${VERSION}/qortal.exe)
\`MD5: ${EXE_MD5}\` qortal.exe
\`SHA1: ${EXE_SHA1}\` qortal.exe
\`SHA256: ${EXE_SHA256}\` qortal.exe
[VirusTotal report for qortal.exe](https://www.virustotal.com/gui/file/${EXE_SHA256}/detection)
### [qortal.zip](https://github.com/Qortal/qortal/releases/download/v${VERSION}/qortal.zip)
Contains bare minimum of:
* built \`qortal.jar\`
* \`log4j2.properties\` from git repo
* \`start.sh\` from git repo
* \`stop.sh\` from git repo
* \`qort\` script for linux/mac easy API utilization
* \`printf "{\n}\n" > settings.json\`
All timestamps set to same date-time as commit.
Packed with \`7z a -r -tzip qortal.zip qortal/\`
\`MD5: ${ZIP_MD5}\` qortal.zip
\`SHA1: ${ZIP_SHA1}\` qortal.zip
\`SHA256: ${ZIP_SHA256}\` qortal.zip
EOF
echo "Release notes generated: release-notes.txt"

View File

@ -0,0 +1,234 @@
#!/usr/bin/env python3
import argparse
import subprocess
import requests
import json
import os
import sys
import time
import hashlib
from pathlib import Path
def run(cmd, cwd=None, capture_output=True):
result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=capture_output, text=True)
if result.returncode != 0:
print(f"Command failed: {cmd}\n{result.stderr}")
sys.exit(1)
return result.stdout.strip()
def get_project_name():
pom = Path('pom.xml')
if not pom.exists():
sys.exit("pom.xml not found!")
for line in pom.read_text().splitlines():
if '<artifactId>' in line:
return line.strip().split('>')[1].split('<')[0]
sys.exit("artifactId not found in pom.xml")
def get_commit_info(commit_hash=None, dry_run=False):
if not commit_hash:
print("No commit hash provided, detecting most recent auto-update branch...")
run("git fetch origin") # Ensure up-to-date
# Get latest auto-update branch by commit date
branches = run("git for-each-ref --sort=-committerdate --format='%(refname:short)' refs/remotes/origin/")
for branch in branches.splitlines():
branch = branch.strip().strip("'")
if branch.startswith("origin/auto-update-"):
commit_hash = branch.replace("origin/auto-update-", "")
print(f"Found latest auto-update branch: {branch}")
break
if not commit_hash:
sys.exit("No auto-update branches found.")
# Validate and get timestamp
if not commit_exists(commit_hash):
sys.exit(f"Commit hash '{commit_hash}' does not exist.")
timestamp = int(run(f"git show --no-patch --format=%ct {commit_hash}")) * 1000
# Use the remote branch hash if available
try:
update_hash = run(f"git rev-parse origin/auto-update-{commit_hash}")
except SystemExit:
print(f"⚠️ Warning: remote branch origin/auto-update-{commit_hash} not found, using commit hash itself.")
update_hash = run(f"git rev-parse {commit_hash}")
return commit_hash, timestamp, update_hash
def commit_exists(commit_hash):
try:
run(f"git cat-file -t {commit_hash}")
return True
except SystemExit:
return False
def get_sha256(update_file_path):
sha256 = hashlib.sha256()
with open(update_file_path, 'rb') as f:
sha256.update(f.read())
return sha256.hexdigest()
def get_public_key(base58_privkey, port):
r = requests.post(f"http://localhost:{port}/utils/publickey", data=base58_privkey)
r.raise_for_status()
return r.text
def get_hex_key(base58_key, port):
r = requests.post(f"http://localhost:{port}/utils/frombase58", data=base58_key)
r.raise_for_status()
return r.text
def get_address(pubkey, port):
r = requests.get(f"http://localhost:{port}/addresses/convert/{pubkey}")
r.raise_for_status()
return r.text
def get_reference(address, port):
r = requests.get(f"http://localhost:{port}/addresses/lastreference/{address}")
r.raise_for_status()
return r.text
def to_base58(hex_str, port):
r = requests.get(f"http://localhost:{port}/utils/tobase58/{hex_str}")
r.raise_for_status()
return r.text
def sign_transaction(privkey, tx_base58, port):
payload = json.dumps({"privateKey": privkey, "transactionBytes": tx_base58})
headers = {"Content-Type": "application/json"}
r = requests.post(f"http://localhost:{port}/transactions/sign", data=payload, headers=headers)
r.raise_for_status()
return r.text
def process_transaction(signed_tx, port):
r = requests.post(f"http://localhost:{port}/transactions/process", data=signed_tx)
r.raise_for_status()
return r.text == 'true'
def decode_transaction(signed_tx, port):
r = requests.post(f"http://localhost:{port}/transactions/decode", data=signed_tx, headers={"Content-Type": "application/json"})
r.raise_for_status()
return r.text
def main():
import getpass
parser = argparse.ArgumentParser(description="Modern auto-update publisher for Qortal")
parser.add_argument("arg1", nargs="?", help="Private key OR commit hash")
parser.add_argument("arg2", nargs="?", help="Commit hash if arg1 was private key")
parser.add_argument("--port", type=int, default=12391, help="API port")
parser.add_argument("--dry-run", action="store_true", help="Simulate without submitting transaction")
args = parser.parse_args()
# Handle combinations
if args.arg1 and args.arg2:
privkey = args.arg1
commit_hash = args.arg2
elif args.arg1 and not args.arg2:
commit_hash = args.arg1
privkey = getpass.getpass("Enter your Base58 private key: ")
else:
commit_hash = None # Will auto-resolve from HEAD
privkey = getpass.getpass("Enter your Base58 private key: ")
# Switch to repo root
git_root = run("git rev-parse --show-toplevel")
os.chdir(git_root)
project = get_project_name()
# Resolve and verify commit
commit_hash, timestamp, update_hash = get_commit_info(commit_hash, args.dry_run)
if not commit_exists(commit_hash):
sys.exit(f"Commit hash '{commit_hash}' does not exist in this repo.")
print(f"Commit: {commit_hash}, Timestamp: {timestamp}, Auto-update hash: {update_hash}")
def get_sha256(update_file_path):
sha256 = hashlib.sha256()
with open(update_file_path, 'rb') as f:
sha256.update(f.read())
return sha256.hexdigest()
update_file = Path(f"{project}.update")
if not update_file.exists():
print(f"{project}.update not found locally. Attempting to restore from branch auto-update-{commit_hash}...")
try:
restore_cmd = f"git show auto-update-{commit_hash}:{project}.update > {project}.update"
run(restore_cmd)
print(f"✓ Restored {project}.update from branch auto-update-{commit_hash}")
except Exception as e:
sys.exit(f"Failed to restore {project}.update: {e}")
# Final check to ensure the file was restored
if not update_file.exists():
sys.exit(f"{project}.update still not found after attempted restore")
sha256 = get_sha256(update_file)
print(f"Update SHA256: {sha256}")
if args.dry_run:
print("\n--- DRY RUN ---")
print(f"Would use timestamp: {timestamp}")
print(f"Would use update hash: {update_hash}")
print(f"Would use SHA256: {sha256}")
sys.exit(0)
pubkey = get_public_key(privkey, args.port)
pubkey_hex = get_hex_key(pubkey, args.port)
address = get_address(pubkey, args.port)
reference = get_reference(address, args.port)
reference_hex = get_hex_key(reference, args.port)
data_hex = f"{timestamp:016x}{update_hash}{sha256}"
if len(data_hex) != 120:
sys.exit("Data hex length invalid!")
raw_tx_parts = [
"0000000a", # type 10 ARBITRARY
f"{int(time.time() * 1000):016x}", # current timestamp
"00000001", # dev group ID
reference_hex, # reference
pubkey_hex, # pubkey
"00000000", # nonce
"00000000", # name length
"00000000", # identifier length
"00000000", # method (PUT)
"00000000", # secret length
"00000000", # compression
"00000000", # number of payments
"00000001", # service ID
"01", # data type (RAW_DATA)
f"{int(len(data_hex)//2):08x}", # data length
data_hex, # payload
f"{int(len(data_hex)//2):08x}", # repeated data length
"00000000", # metadata hash length
f"{int(0.01 * 1e8):016x}" # fee
]
tx_hex = "".join(raw_tx_parts)
tx_base58 = to_base58(tx_hex, args.port)
signed_tx = sign_transaction(privkey, tx_base58, args.port)
print("Submitting in 5 seconds... press CTRL+C to cancel")
for i in range(5, 0, -1):
print(f"{i}...", end='\r', flush=True)
time.sleep(1)
if not process_transaction(signed_tx, args.port):
sys.exit("Transaction submission failed")
decoded = decode_transaction(signed_tx, args.port)
print("\nTransaction submitted successfully:")
print(decoded)
if __name__ == "__main__":
main()